Home > Software engineering >  Understanding libunifex's member_t template metaprogramming facility
Understanding libunifex's member_t template metaprogramming facility

Time:09-29

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 constness 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.)

  • Related