4

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.

Community
  • 1
  • 1
Tomilov Anatoliy
  • 15,657
  • 10
  • 64
  • 169
  • To start with, this breaks if the type has a deleted destructor or cannot be returned from a function (e.g., abstract types, array types, function types). – T.C. Feb 21 '16 at 05:57
  • @T.C. I think for these types *ternary operator* from current default implementation of `std::common_type` also does not make sense, but specializations does. – Tomilov Anatoliy Feb 21 '16 at 17:06
  • 1
    @T.C. Really we just need a language feature that is "give me an expression of type `T`" – Barry Feb 21 '16 at 21:06
  • FWIW I’ve been using my own `common_type` for a while in terms of `template T decltemp();` (i.e. a different name so that it stands out better) since I agree it’s a more sensible spec. That being said, I don’t have *hard* arguments as to why it’ s better so I can’t really answer your question. – Luc Danton Feb 23 '16 at 03:33

1 Answers1

3

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

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 2
    "give me something of type T" That's exactly what `declval` does. It gives you an lvalue of type `T` if you give it `T&`, and an xvalue (modulo `void` (prvalue) and function types (lvalue)) of type `T` if you give it `T` or `T&&`. If you want to use prvalues, that comes with its own pitfalls - in addition to the examples I gave before, it discards cv-qualifiers on non-class types. – T.C. Feb 22 '16 at 03:03