I'm currently trying to understand the std::execution
proposal by studying libunifex's implementation. The library makes heavy use of template metaprogramming.
There's one piece of code I really have trouble understanding:
template <class Member, class Self>
Member Self::* _memptr(const Self&);
template <typename Self, typename Member>
using member_t = decltype(
(std::declval<Self&&>() .*
_memptr<Member>(std::declval<Self&&>())));
What exactly is member_t
used for? It's used in the implementation of then
:
template <typename Sender, typename Receiver>
requires std::same_as<std::remove_cvref_t<Sender>, type> &&
receiver<Receiver> &&
sender_to<member_t<Sender, Predecessor>, receiver_t<std::remove_cvref_t<Receiver>>>
friend auto tag_invoke(tag_t<connect>, Sender&& s, Receiver&& r)
noexcept(
std::is_nothrow_constructible_v<std::remove_cvref_t<Receiver>, Receiver> &&
std::is_nothrow_constructible_v<Function, member_t<Sender, Function>> &&
is_nothrow_connectable_v<member_t<Sender, Predecessor>, receiver_t<std::remove_cvref_t<Receiver>>>
// ----------------------^ here
)
-> connect_result_t<member_t<Sender, Predecessor>, receiver_t<std::remove_cvref_t<Receiver>>> { /* ... */ }
CodePudding user response:
_memptr
is a function declared in such a way that
_memptr<Member>(self)
will give you a pointer to member of <class type of self
> of type Member
regardless of self
's const
ness or value category. So if Self
is possibly a reference type,
Member Self::*
would be ill-formed, but decltype(_memptr<Member>(std::declval<Self&&>()))
will still give you the type you want.
The result of applying std::declval<Self&&>() .*
to the member pointer is that it will produce the referenced member with the value category one would expect from a member access expression via .
when the left-hand side has the value category indicated by Self
's reference-qualification. That means if Self
is a lvalue reference the expression will also be a lvalue and if Self
is a rvalue reference or a non-reference the result will be a xvalue (rvalue).
Applying decltype
to the expression gives you correspondingly a lvalue or rvalue reference to the member's type. It tells you whether, given the value category of self
as a forwarding reference with template parameter Self
its member of type Member
should be moved from or instead copied where necessary.
Now if you have e.g.
template<typename T>
void f(T&& t) {
auto s = (member_t<T, decltype(t.s)>)(t.s);
}
where s
is a non-reference non-static data member of T
, then s
will be copy-constructed from t.s
if f
is passed a lvalue and move-constructed from it if passed a rvalue. Essentially it is std::forward
for the member.
In your shown code it is not directly used this way, but instead the member's type with correct reference-qualification is passed to some type trait e.g. in std::is_nothrow_constructible_v<Function, member_t<Sender, Function>>
to to check whether Function
can be (nothrow) constructed from a Function
lvalue or rvalue depending on whether or not s
was passed a lvalue or rvalue.
It think this performs an equivalent action to what std::forward_like
in C 23 will do when used as
template <typename Self, typename Member>
using member_t = decltype(std::forward_like<Self>(std::declval<Member>()));
(I would not call that template metaprogramming by the way. Nothing here is using template instantiations to implement some algorithm at compile-time.)