Fundamentals 17 min read

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.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Understanding C++ Customization Points: CPO, tag_invoke, and Ranges

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<class _Ty>
            static consteval auto _Choose() noexcept {
                if constexpr (is_array_v<remove_reference_t<_Ty>>)
                    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<class _Ty> 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 <iostream>
#include <ranges>
#include <type_traits>

namespace tag_invoke_test {
    template<auto& CPO>
    using tag_t = std::remove_cvref_t<decltype(CPO)>;

    void tag_invoke();

    template<class CPO, class... Args>
    using tag_invoke_result_t = decltype(tag_invoke(std::declval<CPO&&>(), std::declval<Args&&>()...));

    template<class CPO, class... Args>
    concept nothrow_tag_invocable = noexcept(tag_invoke(std::declval<CPO&&>(), std::declval<Args&&>()...));

    struct example_cpo {
        template<typename T>
        friend bool tag_invoke(example_cpo, const T& x) noexcept { return false; }

        template<typename T>
        auto operator()(const T& x) const noexcept(nothrow_tag_invocable<example_cpo, const T&>)
            -> tag_invoke_result_t<example_cpo, const T&> {
            return tag_invoke(example_cpo{}, x);
        }
    };
    inline constexpr example_cpo example{};

    struct my_type {
        friend bool tag_invoke(tag_t<example>, 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

generic programmingC++CPOCustomization Point ObjectRangestag_invoke
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.