It is used to implement customization points.
Lets start with the "traditional" way to create a cpo:
constexpr struct my_cpo_type {
decltype(auto) operator()(auto&&...args) const {
return my_cpo(std::forward<decltype(args)>(args)...);
}
} my_cpo;
Now, I can do a my_cpo(std::vector<int>{1,2,3})
, and ADL lookup in the body of my_cpo_type::operator()
can find an implementation of my_cpo
specific to the arguments passed. This can be found within the namespace of my_cpo_type
or (if it wasn't illegal) in the namespace of std::vector
or (if it existed) in the namespace of int
.
The object my_cpo
and overload set of the function my_cpo
both have the same name, so that can be a bit confusing in how it works, but the use is pretty clear; self documenting even.
The problem is that the names of the cpo function overload set are effectively reserved in every namespace, because if you have a function with that name it could accidentally be picked up by the cpo
object and generate nonsense.
To address this problem, a meta cpo is created. The tag-dispatch based tag_invoke
meta-cpo acts like a cpo, but its purpose is to allow you to implement other cpos without infecting every namespace with a demand for your specific function name has special meaning.
How does this work?
You start with a customization point object:
namespace mylib{
constexpr struct MyCpo {
auto operator()(auto&&...)const;
} foo;
}
(The operator()
is incomplete).
Users just do:
mylib::foo(some arguments)
and forget about it. Implementors do:
namespace other {
struct bar {
friend int tag_invoke(std::tag_t<mylib::foo>, bar){
return 7;
}
};
}
to say "here is my implementation of mylib::foo(bar{});
. This is the customization point.
tag_invoke
object and name is merely used as a tool to allow this customization without poluting every namespace with piles of new special names like foo
: instead, everyone routes through one special name tag_invoke
.
The implementation of mylib::foo::operator()
is:
template<class...Ts>
decltype(auto) operator()(Ts&&...ts)const{
return std::tag_invoke(std::tag_t<foo>{}, std::forward<Ts>(ts)...);
}
(with maybe some upgrades, like an explicit ->
return type). This then invokes tag_invoke
, which finds the friend of bar
with a matching tag_t
overload, and calls it.
So
Bar b;
std::cout << mylib::foo(b);
prints 7.
mylib::foo
is a customization point object (cpo). It is impemented using the meta-cpo tag_invoke
. The customization of the behavior of mylib::foo
is implemented by overloading tag_invoke( tag_t<mylib::foo>, Bar )
in the namespace of Bar
- in particular, I used the friend technique to make it only considered when an argument is actually Bar
(or is a template with Bar
as a type), which reduces error message spam and probably speeds up lookup.
Every "traditional" cpo has both an object named X
and an set of overloaded functions called X
. The object X
dispatches to the overload set of X
to find the actual behavior. tag_invoke
replaces this with a single cpo that can handle customization points for any number of cpos; this is because we the overload set name isn't namespace restricted, and we don't want to pollute everyone's namespace with a demand that our specific token have a specific meaning.
std::tag_invoke
to implementation. For immunity against accidental overload instd
namespace, it has to be a CPO. It can be implemented as a function pointer/reference or a closure or a monostate. Using thestd::tag_invoke
inside a CPO definition is not essential, but constraints the implementations to use certain signature for their dependenttag_invoke
definitions. Leting a CPO directly use ADL would invite future bugs into the code, by laxing the signature oftag_invoke
. – Stealagetag_invoke
, then ADL must happen somewhere to pick the appropriate definition, no? – Tocologystd::tag_invoke
delegates the task to ADLtag_invoke(std::tag_t, args...)
; thus the call signature is dictated by the CPO definition, while implementation is left to user code. Namespaces are assumed to be the boundaries between libraries; Every well-defined library must break into multiple nested namespaces. – Stealagestd::tag_invoke
, then the implementation ofstd::tag_invoke
is supposed to delegate to(invoke) ADL, but fix the very first argument tostd::tag_t<CPO>
; so the ADL implementedtag_invoke
must use the correct tag as first argument. – Stealagemylib::foo
is the CPO definition, so it defines its invocation as a call tostd::tag_invoke(std::tag_t<foo>{}, args...)
. Andbar
wantsfoo(bar{})
to work; so it definesfriend int tag_invoke(std::tag_t<mylib::foo>, bar);
as ADL function; the first argument has to bestd::tag_t<mylib::foo>
.std::tag_t
is essentially an identity type, but it creates the minimum required type-safety and code readability. – Stealagestd::tag_invoke
could be. – Tocology