Original link: https://twistoy.com/posts/cpo/
Solve what problem?
The title is to talk about some new and interesting new mechanisms, or new wheels, brought by C++20.
An abstraction used to resolve the behavior of library functions or some generic functions to customize the behavior of user types.
For example, I now want to implement a general algorithm:
template<typename T> void foo(T& vec);
I may need type T
for my parameter vec
. You can get his two corresponding iterators and sizes. (here is just an example)
At this point, it’s my generic function that needs to customize the behavior of the user type. In other words, the type of user that needs to provide the capabilities I need.
For each such requirement on user capabilities, it is called a customization point .
What plans are there now?
inherit
The first solution is obviously inheritance, consider this example:
class ConnectionBase { void on_buffer_received(std::span<char> buffer) { if (handle_buffer(buffer)) { // ... } } protected: virtual bool handle_buffer(std::span<char> buffer) = 0; }; class TlsConnection : public ConnectionBase { protected: bool handle_buffer(std::span<char> buffer) { // ssl... } };
The base class’s pure virtual function ConnectionBase::handle_buffer(std::span<char> buffer)
is a customization point . It needs the user type here, ie its subclasses define the behavior of how to handle the received buffer.
CRTP (Curiously Recurring Template Pattern)
Or consider the above example:
template<typename D> class ConnectionBase { void on_buffer_received(std::span<char> buffer) { if (static_cast<D*>(this)->handle_buffer(buffer)) { // ... } } }; class TlsConnection : public ConnectionBase<TlsConnection> { bool handle_buffer(std::span<char> buffer) { // ssl... } };
In this way, the base class can directly access the function and implementation of the subclass, and it has better performance by bypassing the polymorphic call that relies on the virtual table implementation.
The disadvantage is that in this case, the customization points become very inconspicuous, and he can express which customization points the subclass needs to implement without relying on any declaration in the base class.
ADL (Argument-Dependent Lookup)
Still the above example:
namespace tls { class TlsConnection {}; bool handle_buffer(TlsConnection* conn, std::span<char> buffer); // #1 } namespace tcp { class TcpConnection {}; bool handle_buffer(TcpConnection* conn, std::span<char> buffer); // #2 } tls::TlsConnection* tls; tcp::TcpConnection* tcp; handle_buffer(tls); // #1 handle_buffer(tcp); // #2
Under this scheme, the corresponding customization function is implemented in the namespace
of the corresponding parameter, and then the correct implementation is found through ADL
.
A problem brought by such a scheme is that for this handle_buffer
, we may bring different results namespace
namespace
What does the new program look like?
In C++20, in fact, ranges also face a similar problem. If you need to be compatible with various types of containers, you need to customize the corresponding functions for each type of container. The solution given by ranges is CPO, customization point object.
A customization point object is a function object with a literal class type that interacts with program-defined types while enforcing semantic requirements on that interaction.
CPO has good generic compatibility, and can also make up for the problems mentioned in ADL above.
Consider a begin iterator
problem that takes a container of any type:
namespace _Begin { class _Cpo { enum class _St { _None, _Array, _Member, _Non_member }; template <class _Ty> static _CONSTEVAL _Choice_t<_St> _Choose() noexcept { if constexpr (is_array_v<remove_reference_t<_Ty>>) { return {_St::_Array, true}; } else if constexpr (_Has_member<_Ty>) { return {_St::_Member, noexcept(_Fake_decay_copy(_STD declval<_Ty>().begin()))}; } else if constexpr (_Has_ADL<_Ty>) { return {_St::_Non_member, noexcept(_Fake_decay_copy(begin(_STD declval<_Ty>())))}; } else { return {_St::_None}; } } template <class _Ty> static constexpr _Choice_t<_St> _Choice = _Choose<_Ty>(); public: template <_Should_range_access _Ty> requires (_Choice<_Ty&>._Strategy != _St::_None) _NODISCARD 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(); // #1: via member function } else if constexpr (_Strat == _St::_Non_member) { return begin(_Val); // #2: via ADL } else { static_assert(_Always_false<_Ty>, "Should be unreachable"); } } }; } inline namespace _Cpos { inline constexpr _Begin::_Cpo begin; }
This is the implementation of begin
in the ranges library, and I only left the key part to illustrate the problem. In this implementation, most of the logic for type selection is handled by if constexpr
. The operation of seeking begin
on a container type is divided into three cases, array, member function, and non-member function. If the array is a builtin type, it’s not enough to talk about it; the calling of member functions is similar to the usage of CRTP above, because the type of the current container is clearly obtained through the template, and the members of the corresponding custom type can be called directly; the calling of non-member functions Because the name of begin
is used directly, the above ADL method is used here to find the correct processing function for the corresponding container custom type.
The reason why it is a function object instead of a template function is because the function object will not be applied by ADL in this case, that is to say, it will not be found by ADL, avoiding the problem of ADL recursive call in #2
. .
references:
- C++ Draft: customization point object
- Cppreference: ranges
- Niebloids and Customization Point Objects
This article is reprinted from: https://twistoy.com/posts/cpo/
This site is for inclusion only, and the copyright belongs to the original author.