16

The template class std::common_type calculates a common type to a variadic type list. It is defined using the return type of the ternary operator x:y?z recursively. From that definition it is not obvious to me, whether calculating a std::common_type<X,Y> is associative, i. e. whether

using namespace std;
static_assert( is_same<common_type< X, common_type<Y,Z>::type    >::type,
                       common_type<    common_type<X,Y>::type, Z >::type>::value, "" );

will never throw a compile-time error for all types X, Y and Z for which the is_same<...> expression is valid.

Please note, that I'm NOT asking whether

static_assert( is_same<common_type<X,Y>::type,
                       common_type<Y,X>::type>::value, "" );

will ever fire. It will obviously not. The above is a whole different question.

Please note also, that the specification of std::common_type slightly changed in C++14 and will probably change again in C++17. So the answers may be different for different versions of the standard.

Ralph Tandetzky
  • 22,780
  • 11
  • 73
  • 120
  • 2
    Will you accept a case where `(X,(Y,Z))` has a common type, but `(X,Y)`, and hence `(X,Y),Z`, does not? Or do you want them both to resolve to a type, but different ones. 'Cause I'm pretty stuck trying to construct a case for the latter, and strongly suspect it isn't possible; any two chains of conversions will cause an ambiguity. – BoBTFish Dec 22 '15 at 11:36
  • It's certainly the case that the static_assertion can fail to compile, because there is no common type. I assume the question is asking about the case where all the `common_type` types are valid, but `is_same` is false. – Jonathan Wakely Dec 22 '15 at 11:40
  • @JonathanWakely Thanks for the hint. Fixed the question. – Ralph Tandetzky Dec 22 '15 at 11:57
  • Apparently you can make it not-associative. But that doesn't matter, if it not associative it is because there is a logical inconsistency in your design. It would be like defining `operator==` in a way that is not consistent with logic. – alfC Dec 22 '15 at 14:07
  • I assumed this was more of an academic interest, but either way, I disagree with "that doesn't matter... there is a logical inconsistency in your design" because (in the general sense) associativity is not always desired, and substitution failure can be. I can at least think of a tangential counter-example to the `operator==` argument: a double cast down to a float may equal another float, but a float cast up to a double can easily fail equality. (Using this impl. warrants documentation, but casting both to LCM (float here) or both to GCM (double here) to enforce assoc. is edge case spackle.) – John P Oct 28 '17 at 14:59
  • (cont.) - this makes more sense if you have composites (possibly heterogeneous), symbols without closed-form evaluations, types prone to 'gimbal lock' in intermediate representations, etc. - you would be justified to point to conventional arithmetic transitivity/associativity/etc. for the simple example, but maybe not once the other 'intuitive' properties break down. And before you point it out - of course this would all amount to nit-picking in any other context, but not when you're ironing out rigorously-defined properties and hard guarantees. If "you can make it [non]-assoc.", *it isn't*. – John P Oct 28 '17 at 15:21
  • Oh, and sorry for the tone and rant, I haven't slept. Hopefully I at least made sense. – John P Oct 28 '17 at 15:27

3 Answers3

11

This fails on MinGW-w64(gcc 4.9.1). Also fails on VS2013 and (thanks Baum mit Augen) on gcc5.2 or clang 3.7 with libc++.

#include <type_traits>

using namespace std;

struct Z;
struct X{operator Z();};
struct Y{operator X();};
struct Z{operator Y();};

static_assert( is_same<common_type<X,Y>::type,
                       common_type<Y,X>::type>::value, "" ); // PASS

static_assert( is_same<common_type<X,Z>::type,
                       common_type<Z,X>::type>::value, "" ); // PASS

static_assert( is_same<common_type<Y,Z>::type,
                       common_type<Z,Y>::type>::value, "" ); // PASS

static_assert( is_same<common_type< X, common_type<Y,Z>::type    >::type,
                       common_type<    common_type<X,Y>::type, Z >::type>::value, "" ); // FAIL...
Not a real meerkat
  • 5,604
  • 1
  • 24
  • 55
5
#include <type_traits>

struct T2;
struct T1 {
    T1(){}
    T1(int){}
    operator T2();
};
struct T2 {
    operator int() { return 0; }
};
struct T3 {
    operator int() { return 0; }
};
T1::operator T2() { return T2(); }

using namespace std;
using X = T1;
using Y = T2;
using Z = T3;
int main()
{

    true?T2():T3(); // int
    static_assert(std::is_same<std::common_type_t<T2,
                                                  T3>,
                               int>::value,
                  "Not int");

    true?T1():(true?T2():T3()); // T1
    static_assert(std::is_same<std::common_type_t<T1,
                                                  std::common_type_t<T2,
                                                                     T3>>,
                               T1>::value,
                  "Not T1");

    // -----------------------------------------

    true?T1():T2(); // T2
    static_assert(std::is_same<std::common_type_t<T1,
                                                  T2>,
                               T2>::value,
                  "Not T2");

    true?(true?T1():T2()):T3(); // int
    static_assert(std::is_same<std::common_type_t<std::common_type_t<T1,
                                                                     T2>,
                                                  T3>,
                               int>::value,
                  "Not int");

    // -----------------------------------------

    static_assert( is_same<common_type_t< X, common_type_t<Y,Z>    >,
                           common_type_t<    common_type_t<X,Y>, Z > >::value,
                    "Don't match");
}

Ouch! The mental gymnastics here hurt my head, but I came up with a case that fails to compile, printing "Don't match", with gcc 4.9.2 and with "C++14" (gcc 5.1) on ideone. Now whether or not that is conforming is a different matter...

Now the claim is for class types, std::common_type_t<X, Y> should be either X or Y, but I have coerced std::common_type_t<T2, T3> into converting to int.

Please try with other compilers and let me know what happens!

BoBTFish
  • 19,167
  • 3
  • 49
  • 76
  • 1
    Also fires with clang3.7 + libc++. – Baum mit Augen Dec 22 '15 at 12:25
  • fires with mingw-w64 too. VS2013 triggers all of them (and some other errors) except the "Not T2" one. Granted, VS is not know for its conformity to the Standard... – Not a real meerkat Dec 22 '15 at 12:47
  • My interpretation of the standard suggests that the second case is ill-formed because int and T1 can convert to each other. This makes them ineligible to be operands 2 and 3 of operator:? – Richard Hodges Dec 22 '15 at 12:52
  • 4
    @RichardHodges `int` converts to `T1`. `T1` doesn't convert to `int` (one-user-defined-conversion-only rule). – T.C. Dec 22 '15 at 13:05
2

It's not associative! Here's a program where it fails:

#include <type_traits>

struct Z;
struct X { X(Z); }; // enables conversion from Z to X
struct Y { Y(X); }; // enables conversion from X to Y
struct Z { Z(Y); }; // enables conversion from Y to Z

using namespace std;    
static_assert( is_same<common_type< X, common_type<Y,Z>::type    >::type,
                       common_type<    common_type<X,Y>::type, Z >::type>::value, 
               "std::common_type is not associative." );

The idea is simple: The following diagram shows, what common_type calculates:

    X,Y -> Y
    Y,Z -> Z
    X,Z -> X

The first line is logical, since X can be converted to Y, but not vice versa. The same for the other two lines. Once X and Y are combined and recombined with Z we get Z. On the other hand, combining Y and Z and the combining X with the result gives X. Therefore the results are different.

The fundamental reason for this being possible is that convertibility is not transitive, i. e. if X is convertible to Y and Y convertible to Z it does not follow that X is convertible to Z. If convertibility were transitive, then conversions would work both ways and hence the common_type could not be calculated unambiguously and lead to a compile time error.

This reasoning is independent of the standard version. It applies to C++11, C++14 and the upcoming C++17.

Ralph Tandetzky
  • 22,780
  • 11
  • 73
  • 120
  • Yes, but I have philosophical concerns about three categories than can be mapped into each other but only cyclically. I would say that is not associative in the language but it should be, in the same sense that `operator==` should be implemented in a way that `T a = b; assert(a == b);` – alfC Dec 22 '15 at 14:04