How can I specialize std::common_type<A,B> so that it's naturally commutative?

Viewed 164

std::common_type<T1, ..., TN> is a helper template in C++ which can find the common type which all of T1 ... TN are implicitly convertible to.

According the C++ spec, a user may specialize std::common_type<T1,T2> if certain conditions apply, and:

std::common_type<T1, T2>::type and std::common_type<T2, T1>::type must denote the same type.

However, common_type<T1, T2> might be a very complicated specialization for user types T1 and T2:

namespace std {

    template <typename T1, complicated_constraint_of<T1> T2, ...>
    struct common_type<complicated_expression_of<T1, ...>, complicated_expression_of<T2, ...>> {
        using type = complicated_type_expression_of<T1,T2>;
    };

}

In general, the constraint expressions are not necessarily symmetrical (for example, we might specify that T2 is a base of T1). This means that to preserve symmetry, we'd need to rewrite the entire specialization with T1 and T2 reversed, but doing that without making any mistake is extremely difficult and fragile.

How can I robustly define a commutative specialization of common_type<T1,T2> for my own types?

1 Answers

Here is the C++20 solution I came up with:

// define concept of `common_type<A,B>` existing
template <typename A, typename B>
concept has_in_common_ordered = requires { common_type<A,B>::type; };

namespace std {
    
    // define common_type<A,B> if common_type<B,A>::type exists:
    template <typename A, has_in_common_ordered<A> B>
    struct common_type<A,B> : public common_type<B,A> {};
    
}

Now, with the above, the following should compile:

struct X {};
struct Y {};

namespace std {
    
    template<>
    struct common_type<X,Y> {
        using type = X; // or whatever
    };
    
}

int main() {
    // even though we only specialized for one ordering,
    // both orderings now exist:
    std::common_type<X,Y>::type a;
    std::common_type<Y,X>::type b;
}

I think one of two things is possible:

  1. This is a natural technique that should really just be part of the standard definition of std::common_type.
  2. This is extremely evil and you should never do it, for subtle reasons.

I await someone to tell me which one it is in the comments. ;)

Related