9

I am specializing std::common_type for my type. I defined the following specialization:

common_type<my_type, my_type>

And all is well. Then someone comes along and calls std::common_type<my_type, my_type &>. The default version acts the same if you pass a reference vs. not a reference (as it calls std::decay on the types). However, it doesn't defer to the non-reference version of std::common_type, which I would need to work correctly. Is there a better way than having to do something like this (leaving out rvalue-reference to const for simplicity):

common_type<my_type, my_type>
common_type<my_type, my_type &>
common_type<my_type, my_type const &>
common_type<my_type, my_type volatile &>
common_type<my_type, my_type const volatile &>
common_type<my_type, my_type &&>
common_type<my_type, my_type volatile &&>
common_type<my_type &, my_type>
common_type<my_type const &, my_type>
common_type<my_type volatile &, my_type>
common_type<my_type const volatile &, my_type>
common_type<my_type &&, my_type>
common_type<my_type volatile &&, my_type>
common_type<my_type &, my_type &>
common_type<my_type &, my_type const &>
common_type<my_type &, my_type volatile &>
...

Surely there is a better way? By my count, that is 49 possible versions if we ignore const && and const volatile &&

Note: my_type is actually a class template itself, so the specialize actually looks more like

template<intmax_t lhs_min, intmax_t lhs_max, intmax_t rhs_min, intmax_t rhs_max>
class common_type<my_type<lhs_min, lhs_max>, my_type<rhs_min, rhs_max>>

Where the result is my_type<min(lhs_min, rhs_min), max(lhs_max, rhs_max)>

The solution would be fairly straightforward if I had full control over the primary template definitions, but I obviously cannot change std::common_type.

Morwenn
  • 21,684
  • 12
  • 93
  • 152
David Stone
  • 26,872
  • 14
  • 68
  • 84
  • Did you take a look at what your standard library's implementation of `std::common_type` does? – Casey Nov 23 '13 at 21:19
  • The implementation doesn't have much freedom here. It must call `std::decay` (or else have an implementation indistinguishable from doing so). And sure enough, that is what gcc 4.8.2 does – David Stone Nov 23 '13 at 21:22
  • It is somehow implied, but just to make sure: You cannot use something different than `std::common_type`, i.e. change the "call site"? And you cannot define SFINAE-protected conversion operator templates to do the `my_type` "naturally"? – dyp Nov 23 '13 at 22:55
  • I could force all users to use a special `common_type` at the call site, but I am trying to write a general purpose library. I cannot expect all of my users (and all of the libraries that they may use) to use my special `common_type` in general: that defeats the purpose of generic code. I cannot define conversion operators / implicit constructors that do the job because C++ assumes you want to convert one of the types into the other. The common type of two of my classes is typically a third type, which cannot be handled by `operator?:`. – David Stone Nov 23 '13 at 23:55
  • The following does not even compile under g++: `typename std::common_type, volatile std::chrono::duration>::type x;` so you try to provide more genericity than the standard library itself... The best solution, I think is to provide a typedef `::type` into your class and a single version `common_type`. – Vincent Nov 24 '13 at 14:57
  • 1
    I think you could at least reduce the problem to all cv-qualified reference types on *one* side of the binary `common_type` by using a partial specialization instead of a explicit (full) specialization, something like `template common_type;` and then check if after removing the reference, `T` is a specialization of `my_type` (redirect via inheritance). – dyp Nov 26 '13 at 11:41
  • this is perhaps a horrible suggestion but... do you really need this? can you not provide your own 'common_type' implementation with a different name? or even better, just never use such a thing at all? i've never found myself wanting it (this is the first i've heard of it). – jheriko Nov 27 '13 at 03:29
  • @jheriko This construct is used pretty regularly in template metaprogramming contexts, and in that situation, you don't always know what type you are dealing with. In other words, you don't know that you need to use the special `my_common_type`. This situation arose in my own code because I didn't provide any reference overloads, and then when I tried to get the result type of some expression to stick in an array that has its type computed as the common type of all the elements, some of the initializers were references and some were values. – David Stone Nov 27 '13 at 04:03
  • Theoretically, the template metaprogramming community could adopt a similar convention to the usual recommendation of saying `using std::swap; swap(blah, bloo);` instead of calling `std::swap` directly to look up user-defined swap, but it would be even uglier. You would need a using declaration (which cannot appear in template parameters), and then we would require everyone to do something like `decltype(common_type_function(std::declval(), std::declval()))`, where the default declaration of `common_type_function` returns `std::common_type_t`. This is pretty unlikely. – David Stone Nov 27 '13 at 04:10
  • i do plenty of template metaprogramming, but i've never needed this - maybe i misunderstand your point, but one of the great advantages of tmp is allowing the compiler to know the type of everything at compile-time. provided that you never use std::common_type or any library construct that relies on it then the problem is negated... you always use your own common_type implementation... aside from that, i still don't see the value of this construct - i'm yet to hit a problem where i need it (hint: i solve a lot of problems) and i can't imagine one where it is necessary. maybe i am naive... – jheriko Nov 27 '13 at 04:15

2 Answers2

2

I would suggest writing the specialization without the cv- and ref- qualifiers, and using a wrapper around std::common_type<>, like this:

template <typename T>
using decay_t = typename std::decay<T>::type;

/* Our wrapper which passes decayed types to std::common_type<>. */
template <typename... T>
using CommonType = std::common_type<decay_t<T>...>;

namespace std {

  /* Specialization for my_type<>. */
  template <intmax_t lhs_min, intmax_t lhs_max,
            intmax_t rhs_min, intmax_t rhs_max>
  struct common_type<my_type<lhs_min, lhs_max>,
                     my_type<rhs_min, rhs_max>> {
    using type = /* ... */;
  };

}  // std

There are already specializations for std::chrono::duration and std::chrono::time_point which only specialize without cv- and ref- qualifiers.

However, those specializations only get used when cv- and ref- qualifiers are not specified. It's not obvious that they aren't being used because,

static_assert(
  std::is_same<std::common_type<const std::chrono::milliseconds,
                                std::chrono::microseconds &&>::type,
               std::chrono::microseconds>::value, "");

works just fine.

I was confused as to how the specialization was being used until I realized that it wasn't. The generic implementation of std::common_type<> uses decltype() on an if-else expression, like this:

template <typename Lhs, typename Rhs>
using common_type_impl_t = 
    decay_t<decltype(true ? std::declval<Lhs>() : std::declval<Rhs>())>;

Note: It uses SFINAE to pick this one if it's successful otherwise leaves type undefined.

Now we can test that this actually works for std::chrono::duration by testing it out,

static_assert(
  std::is_same<common_type_impl_t<const std::chrono::milliseconds,
                                  std::chrono::microseconds &&>,
               std::chrono::microseconds>::value, "");

which passes. Now, throw volatile in there and it'll break just as @Vincent pointed out, which further proves that the specializations are not being used.

So my conclusion is that one of the two will happen:

  • The standard implementation will change such that the specializations will get used even if there are cv- and ref- qualifiers present, in which case you can toss out the CommonType<> wrapper which is trivial anyway, and you've only defined one specialization.
  • The standard implementation does't change, but you've still only defined one specialization and you use CommonType<> instead.
mpark
  • 7,574
  • 2
  • 16
  • 18
  • I'd guess that the `chrono` thing works because of the implicit conversions of `duration`s, not because of specializations of `common_type`. Which, of course, doesn't work for the OP (result type might be a third type). The `volatile` thing doesn't work because the conversion takes a `const&`, not a `const volatile&`. – dyp Nov 27 '13 at 19:36
2

As far as I know, you don't need to completely specialize both sides of the binary common_type. This allows reducing the amount of specializations to 12 for one side. If you only need a common type between specializations of my_type and my_type, than it's sufficient to specialize on one side. Otherwise, you'd had to clone them on the right side, yielding 24 specializations.

struct my_type;
struct unique_t;

#include <type_traits>

template<class L, class R, class = void>
struct mytype_common_type
{
    // not many specializations are required here,
    // as you can use std::decay and don't have to use "Exact Matches"
    using type = unique_t;
};

namespace std
{
    template<class T> struct common_type<my_type, T>
    : mytype_common_type<my_type, T> {};
    template<class T> struct common_type<my_type const, T>
    : mytype_common_type<my_type, T> {};
    template<class T> struct common_type<my_type volatile, T>
    : mytype_common_type<my_type, T> {};
    template<class T> struct common_type<my_type const volatile, T>
    : mytype_common_type<my_type, T> {};

    template<class T> struct common_type<my_type&, T>
    : mytype_common_type<my_type, T> {};
    template<class T> struct common_type<my_type const&, T>
    : mytype_common_type<my_type, T> {};
    template<class T> struct common_type<my_type volatile&, T>
    : mytype_common_type<my_type, T> {};
    template<class T> struct common_type<my_type const volatile&, T>
    : mytype_common_type<my_type, T> {};

    template<class T> struct common_type<my_type&&, T>
    : mytype_common_type<my_type, T> {};
    template<class T> struct common_type<my_type const&&, T>
    : mytype_common_type<my_type, T> {};
    template<class T> struct common_type<my_type volatile&&, T>
    : mytype_common_type<my_type, T> {};
    template<class T> struct common_type<my_type const volatile&&, T>
    : mytype_common_type<my_type, T> {};
}

template<class T>
using Decay = typename std::decay<T>::type;

int main()
{
    static_assert(std::is_same<unique_t,
                    std::common_type<my_type const volatile&&, int>::type
                  >{}, "!");
}
dyp
  • 38,334
  • 13
  • 112
  • 177
  • I am not seeing how I could duplicate it on the right side, actually. Wouldn't that lead to an ambiguous specialization if I define `std::common_type` and `std::common_type`? – David Stone Nov 28 '13 at 02:19
  • 1
    @DavidStone You're right, if you just used the specializations `common_type` and `common_type`, that would be ambiguous. However, you can fix that by using an `enable_if` for the specializations on the right side: `template struct common_type::type, my_type>{}, my_type>::type> : mytype_common_type {};` – dyp Nov 28 '13 at 12:26