Tomilov Anatoliy
Tomilov Anatoliy

Reputation: 16721

T declval() instead of T && declval() for common_type

Isn't it better to use std::declval declared in form:

template< class T > T declval(); // (1)

then current one:

template< class T > T && declval(); // (2)

for std::common_type (possibly with different name only for this current purpose)?

Behaviour of common_type using (1) is closer to the behaviour of the ternary operator (but not using std::decay_t) than the behaviour when using (2):

template< typename T >
T declval();

template <class ...T> struct common_type;

template< class... T >
using common_type_t = typename common_type<T...>::type;

template <class T>
struct common_type<T> {
    typedef T type;
};

template <class T, class U>
struct common_type<T, U> {
    typedef decltype(true ? declval<T>() : declval<U>()) type;
};

template <class T, class U, class... V>
struct common_type<T, U, V...> {
    typedef common_type_t<common_type_t<T, U>, V...> type;
};

#include <type_traits>
#include <utility>

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunevaluated-expression"
int
main()
{
    int i{};
    static_assert(std::is_same< int &, decltype((i)) >{});
    static_assert(std::is_same< int  , std::common_type_t< decltype((i)), decltype((i)) > >{}); 
    static_assert(std::is_same< int &, decltype(true ? i : i) >{});
    static_assert(std::is_same< int &, common_type_t< decltype((i)), decltype((i)) > >{});

    int && k{};
    static_assert(std::is_same< int &&, decltype(k) >{});
    static_assert(std::is_same< int   , std::common_type_t< decltype(k), decltype(k) > >{}); 
    static_assert(std::is_same< int &&, decltype(true ? std::move(k) : std::move(k)) >{}); 
    static_assert(std::is_same< int &&, common_type_t< decltype(k), decltype(k) > >{});
    return 0;
}
#pragma clang diagnostic pop

Live example.

What are downsides of this approach? Is it true, that for (1) in decltype() context type T should be constructible (at all, i.e. should have at least one constructor) and/or destructible?

Reference article said:

For non-specialized std::common_type, the rules for determining the common type between every pair T1, T2 are exactly the rules for determining the return type of the ternary conditional operator in unevaluated context, with arbitrary first argument of type bool and with xvalues of type T1 and T2 (since C++17) std::declval<T1>() and std::declval<T2>() (until C++17) as the second and the third operands. The common type is the result of std::decay applied to the type of the ternary conditional (since C++14).

I think it is very likely the last sentence (emphasized) should be not just since C++14 but also until C++17 to be fair. Otherwise 1st sentence of cite will not holds even after C++17 and some defect will be present.

There is some clarification in should-stdcommon-type-use-stddecay comments regarding std::common_type problems, but it is just background information for the current question.

Upvotes: 4

Views: 775

Answers (1)

Barry
Barry

Reputation: 303206

The advantage of :

template <class T> T&& declval();

is that it works for any type T, whereas simply returning T will not work for types that are no returnable (e.g. functions, arrays) and types that are not destroyable (e.g. private/protected/deleted destructor, abstract base classes).

Of course, the distandvantage is that common_type<int, int> ends up being int&&, and then you need to add decay which makes common_type<int&, int&> be int - which doesn't make sense either. There's just no win here.


Ultimately, I think we just need some language feature that, in an unevaluated context, is "give me something of type T" that works for any T, that really gives you a T (and not a T&&).

Upvotes: 3

Related Questions