Jes
Jes

Reputation: 2684

class member overload (or specialization) based on different enum values

Is it possible to overload or specialize class member functions based on given enum values?

enum class Type {
  TypeA,
  TypeB,
  TypeC
};

class Foo {

  public:

  template <Type t, typename R = std::enable_if_t<t==Type::TypeA, int>>
  R get() {
    return 1;
  }

  template <Type t, typename R = std::enable_if_t<t==Type::TypeB, double>>
  R get() {
    return 2;
  }

  template <Type t, typename R= std::enable_if_t<t==Type::TypeC, float>>
  R get() {
    return 3;
  }
};

Foo foo;

std::cout << foo.get<Type::TypeA>() << std::endl;
std::cout << foo.get<Type::TypeB>() << std::endl;
std::cout << foo.get<Type::TypeC>() << std::endl;

The compile complain about overloading on above code snippet.

Upvotes: 5

Views: 572

Answers (3)

Nir Friedman
Nir Friedman

Reputation: 17704

Your example doesn't compile because defaulted type templated parameters are not part of the function signature. So as far as the compiler is concerned, you are defining the same function multiple times, which is illegal. Instead, you need to use a defaulted non-type template parameter.

class Foo {

  public:

  template <Type t, std::enable_if_t<t==Type::TypeA, int> = 0>
  int get() {
    return 1;
  }

  template <Type t, std::enable_if_t<t==Type::TypeB, int> = 0>
  double get() {
    return 2;
  }

  template <Type t, std::enable_if_t<t==Type::TypeC, int> = 0>
  float get() {
    return 3;
  }
};

Two working solutions are already posted; so why did I post this one? Well, I like it better. To be concrete:

  • It keeps what the function actually takes and returns separate from when it's enabled. This makes the function easier to read and understand. You can pull the code snippet from the second argument into a tiny macro called REQUIRE or similar; this makes it extremely clear what's going on. Neither of the other two answers posted have this property.
  • This technique is more universal; it can be used in slightly different situations with constructors, which don't return anything and cannot be used with the other two solutions

max66's approach of mapping the enum to a type is nice and it's something you should be aware of. It's more appropriate though when you can write the body generically; if you have to write out every implementation body separately anyhow, it only adds boilerplate (IMHO). Also you should be aware that a solution based on specialization is fragile; it doesn't work if there's a second template parameter nor if the class Foo is a class template, because of the restrictions on specialization function templates.

With the macro I mentioned above you could write it this way:

  template <Type t, REQUIRE(t==Type::TypeA)>
  int get() {
    return 1;
  }
  // ...

Which I think is as good as it gets.

Upvotes: 0

Chris Beck
Chris Beck

Reputation: 16204

One pretty standard way to fix it is to put the std::enable_if clause in the return type of the function, rather than in the template parameters like that.

This compiles for me at c++11 standard.

#include <iostream>
#include <type_traits>

enum class Type {
  TypeA,
  TypeB,
  TypeC
};

class Foo {

  public:

  template <Type t>
  typename std::enable_if<t==Type::TypeA, int>::type get() {
    return 1;
  }

  template <Type t>
  typename std::enable_if<t==Type::TypeB, double>::type get() {
    return 2;
  }

  template <Type t>
  typename std::enable_if<t==Type::TypeC, float>::type get() {
    return 3;
  }
};

static_assert(std::is_same<int, decltype( std::declval<Foo>().get<Type::TypeA>())>::value, "");
static_assert(std::is_same<double, decltype( std::declval<Foo>().get<Type::TypeB>())>::value, "");
static_assert(std::is_same<float, decltype( std::declval<Foo>().get<Type::TypeC>())>::value, "");

int main() {

Foo foo;

std::cout << foo.get<Type::TypeA>() << std::endl;
std::cout << foo.get<Type::TypeB>() << std::endl;
std::cout << foo.get<Type::TypeC>() << std::endl;

}

I'm not sure if I can explain in detail why this change makes such a difference for the compiler.

However, consider the following. With your version, although you are never actually instantiating get with two explicit template parameters, technically all three of those member functions templates can "collide" and produce functions with exactly the same name. Because, if you did instantiate get<Type::TypeB, int>, then it would have the same return type, input parameters, and name as get<Type::TypeA>. C++ does not support function template specialization, it would make overload resolution rules very complicated. So having function templates with the potential to collide like this can make the compiler very upset.

When you do it the way I showed, there is no possibility that the templates can collide and produce a function with the same name and signature.

Upvotes: 6

max66
max66

Reputation: 66210

You can avoid the use of std::enable_if mapping the Type and return types specializing a simple struct (Bar, in the following example)

#include <iostream>

enum class Type {
   TypeA,
   TypeB,
   TypeC
};

template <Type t>
struct Bar;

template <>
struct Bar<Type::TypeA>
 { using  type = int; };

template <>
struct Bar<Type::TypeB>
 { using  type = double; };

template <>
struct Bar<Type::TypeC>
 { using  type = float; };


class Foo {

   public:

      template <Type t>
         typename Bar<t>::type get();
};

template <>
Bar<Type::TypeA>::type Foo::get<Type::TypeA> ()
 { return 1; }

template <>
Bar<Type::TypeB>::type Foo::get<Type::TypeB> ()
 { return 2.2; }

template <>
Bar<Type::TypeC>::type Foo::get<Type::TypeC> ()
 { return 3.5f; }


int main ()
 {
   Foo foo;

   std::cout << foo.get<Type::TypeA>() << std::endl;
   std::cout << foo.get<Type::TypeB>() << std::endl;
   std::cout << foo.get<Type::TypeC>() << std::endl;

   return 0;
 }

Upvotes: 3

Related Questions