Home > Software engineering >  Why is std::forward necessary for checking if a type can be converted to another without narrowing i
Why is std::forward necessary for checking if a type can be converted to another without narrowing i

Time:11-17

To make a concept checking if a type can be converted without narrowing to another, it is proposed here to make it using std::forward and std::type_identity_t like this:

template<class T, class U>
concept __construct_without_narrowing = requires (U&& x) {
    { std::type_identity_t<T[]>{std::forward<U>(x)} } -> T[1];
};

I understand from it why something like this:

To{std::declval<From>()}

gives incorrect results, but when i try to simplify it using another idea in the paper, writing just

template <typename From, typename To>
concept WithoutNarrowing = 
requires (From x) {
    {(To[1]){x}} 
    ->std::same_as<To[1]>;
};

It seems to give the same results. What circumstances have to occur for it to give different result? Or is it equivalent? For what reason is std::forward used here?

CodePudding user response:

This is the usual approach for type traits like this that involve some kind of function/constructor argument.

U is the type from which T is supposed to be constructed, but if we want to discuss the construction we also need to consider the value category of the argument. It may be an lvalue or a rvalue and this can affect e.g. which constructor is usable.

The idea is that we map the rvalue argument case to a non-reference U or rvalue reference U and the lvalue argument case to a lvalue reference U, matching the mapping of expressions in decltype and of return types with value categories in function call expressions.

Then, by the reference collapsing rules, U&& will be a lvalue reference if the constructor argument is a lvalue and otherwise a rvalue reference. Then using std::forward means that the actual argument we give to the construction will indeed be a lvalue argument when U was meant to represent one and a rvalue argument otherwise.

Your approach using {(To[1]){x}} doesn't use the forwarding and so would always only test whether construction from a lvalue can be done without narrowing, which is not what is expected if e.g. U is a non-reference.


Your approach is further incorrect because (To[1]){x} is not valid syntax in standard C . If X is a type you can have X{x} or (X)x, but not (X){x}. The last syntax is part of C however and called a compound literal there. For that reason a C compiler may support it as an extension to C . That's why the original implementation uses the round-about way with std::type_identity_t.


The implementation seems to also be written for an earlier draft of C 20 concepts. It is now not possible to use types to the right of -> directly for a requirement. Instead a concept, i.e. -> std::same_as<T[1]>, must be used as in your suggested implementation.

CodePudding user response:

well there is difference between (U u), (U& u) and (U&& u) that std::forward is supposed to preserve. in case of (U u) the type has to have defined a copy constructor (since (U u) basically means "pass a copy of")

  • Related