Understanding C++ Customization Points: CPO, tag_invoke, and Ranges
This article explains how modern C++ libraries achieve extensible interfaces using Customization Point Objects and the tag_invoke mechanism, compares classic techniques, details the implementation of ranges::begin, and shows how tag_invoke provides a scalable, concept‑friendly alternative for generic library design.
This article introduces the concept of customization in C++ and explains how modern C++ libraries use Customization Point Objects (CPOs) and the tag_invoke mechanism to provide extensible interfaces. It starts by discussing the need for customization points in library design, illustrating the difference between user‑implemented points (Point A) and library‑called points (Point B) with a simple diagram.
It then reviews classic techniques such as inheritance, polymorphism, CRTP, and ADL, highlighting their limitations when dealing with generic code. The author uses std::pmr::memory_resource as an example of a polymorphic interface:
class memory_resource {
public:
void* allocate(size_t bytes, size_t align = alignof(max_align_t)) {
return do_allocate(bytes, align);
}
private:
virtual void* do_allocate(size_t bytes, size_t align) = 0;
};
class users_resource : public std::pmr::memory_resource {
void* do_allocate(size_t bytes, size_t align) override {
return ::operator new(bytes, std::align_val_t(align));
}
};Next, the article examines the C++20 std::ranges library, showing how it implements generic algorithms using CPOs. The ranges::begin CPO is presented in detail, demonstrating how it selects the appropriate strategy (array, member begin() , or ADL) via if constexpr :
namespace ranges {
namespace _Begin {
class _Cpo {
private:
enum class _St { _None, _Array, _Member, _Non_member };
template
static consteval auto _Choose() noexcept {
if constexpr (is_array_v
>)
return _St::_Array;
else if constexpr (_Has_member<_Ty>)
return _St::_Member;
else if constexpr (_Has_ADL<_Ty>)
return _St::_Non_member;
else
return _St::_None;
}
public:
template
requires (_Choice<_Ty&>._Strategy != _St::_None)
constexpr auto operator()(_Ty&& _Val) const {
constexpr _St _Strat = _Choice<_Ty&>._Strategy;
if constexpr (_Strat == _St::_Array) return _Val;
else if constexpr (_Strat == _St::_Member) return _Val.begin();
else if constexpr (_Strat == _St::_Non_member) return begin(_Val);
else static_assert(false, "Should be unreachable");
}
};
}
inline namespace _Cpos { inline constexpr _Begin::_Cpo begin; }
}The author then points out that while CPOs solve many problems, their implementation can become complex as the number of customization points grows. To address this, the tag_invoke proposal (P1895R0) is introduced as a cleaner alternative. A minimal example demonstrates how a CPO can delegate its work to a free function tag_invoke :
#include
#include
#include
namespace tag_invoke_test {
template
using tag_t = std::remove_cvref_t
;
void tag_invoke();
template
using tag_invoke_result_t = decltype(tag_invoke(std::declval
(), std::declval
()...));
template
concept nothrow_tag_invocable = noexcept(tag_invoke(std::declval
(), std::declval
()...));
struct example_cpo {
template
friend bool tag_invoke(example_cpo, const T& x) noexcept { return false; }
template
auto operator()(const T& x) const noexcept(nothrow_tag_invocable
)
-> tag_invoke_result_t
{
return tag_invoke(example_cpo{}, x);
}
};
inline constexpr example_cpo example{};
struct my_type {
friend bool tag_invoke(tag_t
, const my_type& t) noexcept { return t.is_example_; }
bool is_example_;
};
}
int main() {
auto val = tag_invoke_test::example(3);
val = tag_invoke_test::example(tag_invoke_test::my_type{ true });
return 0;
}The article explains that each customization point gets its own tag_t type, and user‑defined types can provide overloads of tag_invoke to customize behavior without polluting namespaces. This approach scales well for large libraries such as libunifex.
Finally, the author summarizes the advantages of CPOs and tag_invoke: they enable generic, compile‑time extensibility, reduce boilerplate compared to older tag‑dispatch techniques, and integrate smoothly with concepts for argument validation.
References are provided for the tag_invoke proposal, libunifex source, C++ ranges documentation, and other related reading.
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.