11
votes

For a bit of fun, I created a very basic compile-time type-value map class, as follows:

template <typename T, auto V>
struct TypeValuePair { };

template <typename... TypeValuePairs>
struct TypeValueMap
  {
  struct MapItems : TypeValuePairs... { };

  template <typename T, auto V>
  static constexpr auto Lookup(TypeValuePair<T, V>*)
    { return V; }

  template <auto V, typename T>
  static T Lookup(TypeValuePair<T, V>*);

  template <typename T>
  static constexpr auto ValueFor = Lookup<T>((MapItems*)nullptr);

  template <auto V>
  using TypeFor = decltype(Lookup<V>((MapItems*)nullptr));
  };

to be used in a way such as this:

struct A; struct B; struct C;
enum class Values { A, B, C };

using Map = TypeValueMap<
                TypeValuePair<A, Values::A>,
                TypeValuePair<B, Values::B>,
                TypeValuePair<C, Values::C>,
                TypeValuePair<struct Other, 0>
              >;

static_assert(Map::ValueFor<A> == Values::A, "");
static_assert(Map::ValueFor<B> == Values::B, "");
static_assert(Map::ValueFor<C> == Values::C, "");
static_assert(Map::ValueFor<struct Other> == 0, "");

static_assert(std::is_same<Map::TypeFor<Values::A>, A>::value, "");     //***
static_assert(std::is_same<Map::TypeFor<Values::B>, B>::value, "");
static_assert(std::is_same<Map::TypeFor<Values::C>, C>::value, "");
static_assert(std::is_same<Map::TypeFor<0>, struct Other>::value, "");  //***

Unfortunately, the two lines marked //*** fail with the error failed template argument deduction or similar on clang and g++ (the two compilers I have to hand). I can understand why this might be because Values::A has the value 0 so the two potentially collide. However, I would argue that they are in fact different types – one is plain integer, the other an enum class with underlying type integer – and so shouldn't in fact collide.

If I implement my map class differently, like so:

template <typename T, auto V>
struct TypeValuePair
  {
  protected:
  static constexpr auto Lookup(T*)
    { return V; }

  template <template <auto> class Wrapper>
  static T Lookup(Wrapper<V>*);
  };

template <typename... TypeValuePairs>
struct TypeValueMap
  {
  struct MapItems : TypeValuePairs...
    { using TypeValuePairs::Lookup...; };

  template <auto> struct LookupByValue;

  template <typename T>
  static constexpr auto ValueFor = MapItems::Lookup((T*)nullptr);

  template <auto V>
  using TypeFor = decltype(MapItems::Lookup((LookupByValue<V>*)nullptr));
  };

then there are no template argument deduction errors.

Therefore the question is, is the failure to deduce the template argument in the first implementation due to a bug in the compilers (given my assertion that integer and enum class should be treated as different types and not collide) or is it a misunderstanding on my side of what is possible with template argument deduction (I am not a language lawyer!), or some other bug in my implementation?

1
Unrelated to the main question, but you can simplify: struct MapItems : TypeValuePairs... { using TypeValuePairs::Lookup...; };.Evg
Are you sure clang and gcc produce the same error? What happens with both compilers if you remove A from the map?n. 1.8e9-where's-my-share m.
Thanks @Evg for the suggestion, that's really neat! I've updated the question with this change.Andy G
@n.m. - yes, both compilers produce the same error; if your remove A from the map the "ambiguity" goes and the remaining map resolvesAndy G
Minimal example: godbolt.org/z/nIFKxYEvg

1 Answers

0
votes

The problem is that Values::A is convertible to 0 by means of a "converted constant expression".

From the C++14 standard (n4140 section 5.19, paragraph 3, page 133):

A converted constant expression of type T is an expression, implicitly converted to a prvalue of type T, where the converted expression is a core constant expression and the implicit conversion sequence contains only user-defined conversions, lvalue-to-rvalue conversions (4.1), integral promotions (4.5), and integral conversions (4.7) other than narrowing conversions (8.5.4).

[ Note: such expressions may be used in new expressions (5.3.4), as case expressions (6.4.2), as enumerator initializers if the underlying type is fixed (7.2), as array bounds (8.3.4), and as integral or enumeration non-type template arguments (14.3). —end note ]

(my emphasis)

The effect is that there is an overload ambiguity between 0 and Values::A.