Understanding libunifex's member_t template metaprogramming facility - c++

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>>> { /* ... */ }

_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

how is std::is_function implemented

Per CPP reference, std::is_function can be implemented as follows. Can someone explain why this works as it seemingly does not directly address callables?
template<class T>
struct is_function : std::integral_constant<
bool,
!std::is_const<const T>::value && !std::is_reference<T>::value
> {};
It exploits this sentence from https://eel.is/c++draft/basic.type.qualifier#1
A function or reference type is always cv-unqualified.
So, given a type T, it tries to make a const T. If the result is not a const-qualified type, then T must be a function or reference type. Then it eliminates reference types, and done.
(not to be confused with member functions that have const in the end: that is, in standardese, "a function type with a cv-qualifier-seq", not the same as a "cv-qualified function type")

Ambigous operator overload on clang [duplicate]

Consider the following code, which mixes a member and a non-member operator|
template <typename T>
struct S
{
template <typename U>
void operator|(U)
{}
};
template <typename T>
void operator|(S<T>,int) {}
int main()
{
S<int>() | 42;
}
In Clang this code fails to compile stating that the call to operator| is ambiguous, while gcc and msvc can compile it. I would expect the non-member overload to be selected since it is more specialized. What is the behavior mandated by the standard? Is this a bug in Clang?
(Note that moving the operator template outside the struct resolves the ambiguity.)
I do believe clang is correct marking the call as ambiguous.
Converting the member to a free-standing function
First, the following snippet is NOT equal to the code you have posted w.r.t S<int>() | 42; call.
template <typename T>
struct S
{
};
template <typename T,typename U>
void operator|(S<T>,U)
{}
template <typename T>
void operator|(S<T>,int) {}
In this case the now-non-member implementation must deduce both T and U, making the second template more specialized and thus chosen. All compilers agree on this as you have observed.
Overload resolution
Given the code you have posted.
For overload resolution to take place, a name lookup is initiated first. That consists of finding all symbols named operator| in the current context, among all members of S<int>, and the namespaces given by ADL rules which is not relevant here. Crucially, T is resolved at this stage, before overload resolution even happens, it must be. The found symbols are thus
template <typename T> void operator|(S<T>,int)
template <typename U> void S<int>::operator|(U)
For the purpose of picking a better candidate, all member functions are treated as non-members with a special *this parameter. S<int>& in our case.
[over.match.funcs.4]
For implicit object member functions, the type of the implicit object parameter is
(4.1)
“lvalue reference to cv X” for functions declared without a ref-qualifier or with the & ref-qualifier
(4.2)
“rvalue reference to cv X” for functions declared with the && ref-qualifier
where X is the class of which the function is a member and cv is the cv-qualification on the member function declaration.
This leads to:
template <typename T> void operator|(S<T>,int)
template <typename U> void operator|(S<int>&,U)
Looking at this, one might assume that because the second function cannot bind the rvalue used in the call, the first function must be chosen. Nope, there is special rule that covers this:
[over.match.funcs.5][Emphasis mine]
During overload resolution, the implied object argument is indistinguishable from other arguments.
The implicit object parameter, however, retains its identity since no user-defined conversions can be applied to achieve a type match with it.
For implicit object member functions declared without a ref-qualifier, even if the implicit object parameter is not const-qualified, an rvalue can be bound to the parameter as long as in all other respects the argument can be converted to the type of the implicit object parameter.
Due to some other rules, U is deduced to be int, not const int& or int&. S<T> can also be easily deduced as S<int> and S<int> is copyable.
So both candidates are still valid. Furthermore neither is more specialized than the other. I won't go through the process step by step because I am not able to who can blame me if even all the compilers did not get the rules right here. But it is ambiguous exactly for the same reason as foo(42) is for
void foo(int){}
void foo(const int&){}
I.e. there is no preference between copy and reference as long as the reference can bind the value. Which is true in our case even for S<int>& due to the rule above. The resolution is just done for two arguments instead of one.

Is this a universal reference? Does std::forward make sense here?

Consider this snippet of code, which uses the common idiom of having a function template construct an instance of a class template specialized on a deduced type, as seen with std::make_unique and std::make_tuple, for example:
template <typename T>
struct foo
{
std::decay_t<T> v_;
foo(T&& v) : v_(std::forward<T>(v)) {}
};
template <typename U>
foo<U> make_foo(U&& v)
{
return { std::forward<U>(v) };
}
In the context of Scott Meyers' "universal references", the argument to
make_foo is a universal reference because its type is U&& where U is
deduced. The argument to the constructor of foo is not a universal reference
because although its type is T&&, T is (in general) not deduced.
But in the case in which the constructor of foo is called by make_foo, it
seems to me that it might make sense to think of the argument to the constructor
of foo as being a universal reference, because T has been deduced by the
function template make_foo. The same reference collapsing rules will apply
so that the type of v is the same in both functions. In this case, both T
and U can be said to have been deduced.
So my question is twofold:
Does it make sense to think of the argument to the constructor of foo as being
a universal reference in the limited cases in which T has been deduced
within a universal reference context by the caller, as in my example?
In my example, are both uses of std::forward sensible?
make_foo is in the same ballpark as "right", but foo isn't. The foo constructor currently only accepts a non-deduced T &&, and forwarding there is probably not what you mean (but see #nosid's comment). All in all, foo should take a type parameter, have a templated constructor, and the maker function should do the decaying:
template <typename T>
struct foo
{
T v_;
template <typename U>
foo(U && u) : v_(std::forward<U>(u)) { }
};
template <typename U>
foo<typename std::decay<U>::type> make_foo(U && u)
{
return foo<typename std::decay<U>::type>(std::forward<U>(u));
}
In C++14 the maker function becomes a bit simpler to write:
template <typename U>
auto make_foo(U && u)
{ return foo<std::decay_t<U>>(std::forward<U>(u)); }
As your code is written now, int a; make_foo(a); would create an object of type foo<int &>. This would internally store an int, but its constructor would only accept an int & argument. By contrast, make_foo(std::move(a)) would create a foo<int>.
So the way you wrote it, the class template argument determines the signature of the constructor. (The std::forward<T>(v) still makes sense in a perverted kind of way (thanks to #nodis for pointing this out), but this is definitely not "forwarding".)
That is very unusual. Typically, the class template should determine the relevant wrapped type, and the constructor should accept anything that can be used to create the wrapped type, i.e. the constructor should be a function template.
There isn't a formal definition of "universal reference", but I would define it as:
A universal reference is a parameter of a function template with type [template-parameter] &&, with the intent that the template parameter can be deduced from the function argument, and the argument will be passed either by lvalue reference or by rvalue reference as appropriate.
So by that definition, no, the T&& v parameter in foo's constructor is not a universal reference.
However, the entire point of the phrase "universal reference" is to provide a model or pattern for us humans to think about while designing, reading, and understanding code. And it is reasonable and helpful to say that "When make_foo calls the constructor of foo<U>, the template parameter T has been deduced from the argument to make_foo in a way that allows the constructor parameter T&& v to be either an lvalue reference or an rvalue reference as appropriate." This is close enough to the same concept that I would be fine moving on to the claim: "When make_foo calls the constructor of foo<U>, the constructor parameter T&& v is essentially a universal reference."
Yes, both uses of std::forward will do what you intend here, allowing member v_ to move from the make_foo argument if possible or copy otherwise. But having make_foo(my_str) return a foo<std::string&>, not a foo<std::string>, that contains a copy of my_str is quite surprising....

std::remove_reference explained?

I saw possible implementations for std::remove_reference as below
template< class T > struct remove_reference {typedef T type;};
template< class T > struct remove_reference<T&> {typedef T type;};
template< class T > struct remove_reference<T&&> {typedef T type;};
Why is it that there are specializations for lvalue and rvalue reference? Won't the general template itself be sufficient and remove the reference? I'm confused here because in the T& or T&& specialization if I try to use ::type I should still get T& or T&& respectively right?
Could you explain how, why we cast to remove_reference<t>::type&& in move? (is it because that the parameter is named so it will be treated as an lvalue inside the move function?).
Also, could you point out a way whereby I can find out and print what the type is? for e.g if its an rvalue of type int then I should be able to print out that int&& was passed? (I've been using std::is_same to check but manually.)
Thank you for your time.
why is it that there are specializations for lvalue and rvalue reference?
If only the primary template existed, then doing:
remove_reference<int&>::type
Would give you:
int&
And doing:
remove_reference<int&&>::type
Would give you:
int&&
Which is not what you want. The specializations for lvalue references and rvalue references allow stripping the & and the &&, respectively, from the type argument you pass.
For instance, if you are doing:
remove_reference<int&&>
The type int&& will match the pattern specified by the T&& specialization, with T being int. Since the specialization defines the type alias type to be T (in this case, int), doing:
remove_reference<int&&>::type
Will give you int.
could you explain how, why we cast to remove_reference<t>::type&& in move?
That's because if move() were defined as follows:
template<typename T>
T&& move(T&& t) { ... }
// ^^^
// Resolves to X& if T is X& (which is the case if the input has type X
// and is an lvalue)
Then the return type will be X& if the argument of move() is an lvalue of type X (that's how so-called "universal references"). We want to make sure that the return type is always an rvalue reference.
The purpose of move() is to give you back an rvalue, no matter what you pass in input. Since a function call for a function whose return type is an rvalue reference is an rvalue, we really want move() to always return an rvalue reference.
That's why we do remove_reference<T>::type&&, because appending && to a non-reference type is always guaranteed to yield an rvalue reference type.
Also could you point out a way whereby I can find out and print what the type is?
I'm not sure what you mean by "print" here. There is no portable way I know of converting the name of a type to a string (no matter how you obtain that type).
If your goal is to make sure that an rvalue was passed, on the other hand, you could use a static assertion like so:
#include <type_traits>
template<typename T>
void foo(T&&)
{
static_assert(!std::is_reference<T>::value, "Error: lvalue was passed!");
// ...
}
Which relies on the fact that when an lvalue of type X is being passed, T will be deduced to be X&.
You could also use an equivalent SFINAE-constraint, if you only want to produce a substitution failure:
#include <type_traits>
template<typename T, typename std::enable_if<
!std::is_reference<T>::value>::type* = nullptr>
void foo(T&&)
{
// ...
}
When you treat some type as template parameter, compiler searches for the most "specialized" specialization. If you pass a int&& to this template, compiler use remove_reference<T&&> version. General specialization don't give you what you want - if you pass int&& to general specialiation, type will be int&&
If you want to print type, use typeid(some_type).name()

Const temporary from template type and why use std::add_const?

The following code is excerpted from cppreference.com.
#include <iostream>
#include <type_traits>
struct foo
{
void m() { std::cout << "Non-cv\n"; }
void m() const { std::cout << "Const\n"; }
};
template <class T>
void call_m()
{
T().m();
}
int main()
{
call_m<foo>();
call_m<std::add_const<foo>::type>();
}
However, when compiled with VC++ Nov 2012 CTP, the output is
Non-cv
Non-cv
rather than the expected:
Non-cv
Const
Besides, what's the difference between the following two statements:
call_m<const foo>();
and
call_m<std::add_const<foo>::type>();
This appears to be a bug with MSVC. Using an expression of the form T() (which is an explicit type conversion, as far as the standard is concerned) results in a prvalue of the specified type.
The expression T(), where T is a simple-type-specifier or typename-specifier for a non-array complete object type or the (possibly cv-qualified) void type, creates a prvalue of the specified type, which is value-initialized
It is only with non-class types that the const would be ignored, due to a rule that non-class prvalues cannot have cv-qualified types:
Class prvalues can have cv-qualified types; non-class prvalues always have cv-unqualified types.
So the temporary object created by T() here should be const and should therefore call the const member function.
As for when and why you would use std::add_const, we can take a look at the reason it was included in the proposal. It states that the add_const, add_volatile, add_cv, add_pointer, and add_reference type traits were removed from the proposal but then reinstated after complaints from users of Boost.
The rationale is that these templates are all used as compile time functors which transform one type to another [...]
The example given is:
// transforms 'tuple<T1,T2,..,Tn>'
// to 'tuple<T1 const&,T2 const&,..,Tn const&>'
template< typename Tuple >
struct tuple_of_refs
{
// transform tuple element types
typedef typename mpl::transform<
typename Tuple::elements,
add_reference< add_const<_1> > // here!
>::type refs;
typedef typename tuple_from_sequence<refs>::type type;
};
template< typename Tuple >
typename tuple_of_refs<Tuple>::type
tuple_ref(Tuple const& t)
{
return typename tuple_of_refs<Tuple>::type(t);
}
You can think of mpl::transform as taking the compile-time metaprogramming equivalent to a function pointer as its second template argument - add_reference<add_const<...>> is applied to each of the types in Tuple::elements. This simply couldn't be expressed using const.
From what i remember it goes as follows, you can write 3 consts(3 is the number of purpose) in a function declaration.
before the return type, after the function and its parameters, and on the parameters themselves.
const at the end of a function signature means that the function should assume the object of which it is a member is const. In practical terms it means that you ask the compiler to check that the member function does not change the object data in any way. It means asking the compiler to check that it doesn't directly change any member data, and it doesn't call any function that itself does not guarantee that it won't change the object.
before the return type means the thing the function is to return should be a const.
const parameter means that the parameter cannot be changed.
so the difference here is, the first call is not a const so it goes to the "non-cv", the second call is a const and so goes to "const".
what i think of why VC++ goes to the same function both times is that call_m explicitly calls for T().m() thinking it shouldnt go to the const one.