17

Given types A,B, I am concerned with the exact definition of std::common_type<A,B>, disregarding the variadic case std::common_type<A...> for arbitrary types A.... So let

using T = decltype(true ? std::declval<A>() : std::declval<B>());
using C = std::common_type<A,B>;

Now, according to a number of sources, I have found the following relations (skipping typename for brevity):

  • cppreference.com: C::type = std::decay<T>::type

  • cplusplus.com: C::type = T

  • GCC 4.8.1 <type_traits> implementation: C::type = std::decay<T>::type if T is valid, otherwise C does not contain a ::type member ("SFINAE-friendly")

  • Clang 3.3 <type_traits> implementation: C::type = std::remove_reference<T>::type

I find the "SFINAE-friendly" version of GCC a minor detail, while std::remove_reference and std::decay practically only differ in built-in arrays and functions, plus cv-qualification, for which again I am not concerned much. So my question is

Should it be decay<T>::type or just T? What is the rationale of using decay<T>::type? Is it only about representing result A() + B() e.g. for arithmetic expressions?

For instance, experimenting a bit, I have found that in the case of the "just T" definition, we have

common_type<int&,int&> = int&
common_type<int&,long&> = long

that is, an lvalue reference is maintained if types are equal. This reflects the fact that

int a, b;
(true ? a : b) = 0;

is valid, while

int a;
long b;
(true ? a : b) = 0;

is not. This semantics of "allowing assignment if types are equal" is exactly what I need in one application, and I tend to believe that common_type and decay should be two independent steps. Should I just use my own definitions?

iavr
  • 7,547
  • 1
  • 18
  • 53
  • `remove_reference` and `decay` also differ in the cv-qualification for reference types; `std::declval` returns an rvalue reference. – dyp Feb 23 '14 at 23:09
  • `T` is old (C++11), `decay` is new (C++1y), there's probably a defect related to this. Let me see.. – dyp Feb 23 '14 at 23:11
  • @dyp Yes, I wasn't so careful there, that's why I wrote "practically", I'll fix. – iavr Feb 23 '14 at 23:11
  • 6
    Here it is: http://www.open-std.org/JTC1/SC22/WG21/docs/lwg-defects.html#2141 – dyp Feb 23 '14 at 23:12
  • If you need the correct value category, I'd write my own `common_type_valcat` *based upon* `common_type` (as `common_type` might have been specialized by someone). – dyp Feb 23 '14 at 23:28
  • @dyp The problem is that, according to your answer, all specializations should be supposed to use `decay` as well, after which it's too late to recover the original type given by the ternary operator... Now it seems like a mess to me. – iavr Feb 23 '14 at 23:31
  • 1
    Yeah, if you need a version that can unify identical lvalue reference types as still references, it's probably best to write your own. – aschepler Feb 23 '14 at 23:34
  • @iavr The simplest thing to do is if the types are all lvalue references, return `typename std::common_type::type &` (i.e., add an lvalue reference). However, this isn't enough because of cv-qualification and it gets more complicated once you have types like `struct foo { operator int&(); };` that can be converted from a prvalue `foo` to an lvalue `int`. – dyp Feb 23 '14 at 23:52
  • 1
    Yes, it does get complicated, especially with `foo`. E.g. given `foo f;` you can say `++f;` but not `f = 0;`. Besides this case, I am more inclined to just removing rvalue-references (caused by `declval` or not) and leaving cv-qualification intact. Anyhow, I'll see how it goes with my application. – iavr Feb 24 '14 at 00:27

1 Answers1

14

should std::common_type use std::decay?

Yes, see Library Working Group Defect #2141.

Short version (long version, see link above):

  • declval<A>() returns a A&&

  • common_type is specified via declval, n3337:

    template <class T, class U>
    struct common_type<T, U> {
        typedef decltype(true ? declval<T>() : declval<U>()) type;
    };
    
  • common_type<int, int>::type therefore yields int&&, which is unexpected

  • proposed resolution is to add decay

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

dyp
  • 38,334
  • 13
  • 112
  • 177
  • 1
    Hmm... that shows indeed how we ended up in using `decay`, so it answers the question, thanks a lot. Too bad however, I now have to use my own definitions... Yes, `` should give `int` and not `int&&`, but `` should give `int&` for me. So only additional `&&` added by `declval` should be removed. – iavr Feb 23 '14 at 23:28
  • 1
    @iavr The discussion at the LWG page contains *"Users of `common_type` do not expect to get a reference type as the result"* Not sure who *the users* are, though. – dyp Feb 23 '14 at 23:30
  • Well, not me for sure :-) – iavr Feb 23 '14 at 23:32
  • 4
    Bah, I expected to get a reference if I feed in references, and I'm a user of `common_type`. If I want to implement the equivalent of a many-argument chained `?`, `common_type` now behaves fundamentally differently, and as `common_type` is the only spot where one expects specialization of things like `std::chrono` and other extensions, one can no longer implement a many-argument chained `?`. – Yakk - Adam Nevraumont May 22 '14 at 20:55
  • Ok, I can do binary `common_type` to access said overloads, strip references coming in and add them coming out, and use my own unary and trinary+ implementations for those cases, so this can be recovered from. – Yakk - Adam Nevraumont May 22 '14 at 21:02
  • 1
    I just landed here cause I expected common_type to return an int&. It returned an int, my code silently compiled, and a test failed. Been debugging this for a bit already... lucky I had a test. – gnzlbg Jan 22 '15 at 18:40
  • @Yakk I also do not know why you would expect `common_type_t` to *not* yield `X&`, when that's what the conditional operator would yield... – Barry Sep 21 '15 at 13:36
  • @Barry I expect it to yield `X&`, but the standard disagrees. As far as I can tell, this is because the standard added an over-zealous `decay` when it should have added a `remove_reference_if_rvalue_reference` or somesuch. (`decltype( false?0:0.0 )` evaluates to a `double&&` not a `double`, and whomever added `decay` thought this was surprising and avoided by a `decay`) – Yakk - Adam Nevraumont Sep 21 '15 at 13:37