thayne
thayne

Reputation: 1048

Overloading a function with std::enable_if to avoid template substitution error

I want to write two template functions such that one catches a specific case and the other catches all other cases that don't match first case. I'm trying to use std::enable_if to catch the specific case, but the compiler still fails with an ambiguous match. How can I write these overloaded functions so that the ambiguity is resolved by the compiler? (I'm using g++)

I've tried writing the following code (this is a simplified example that reproduces the problem):

struct resource1_t{
};

struct resource2_t{
};

template <typename R, typename V>
struct unit_t{
  typedef R resource_t;
  typedef V value_t;
  unit_t(value_t const& value):v(){}
  value_t v;
  value_t calcValue(resource_t const& r)const{return v;}
};

// Specific case (U::resource_t == R)
template <typename U, typename R, typename=std::enable_if_t<std::is_same_v<typename U::resource_t,R>>>
      typename U::value_t callCalcValue(U const& u, R const& r){
        return u.calcValue(r);
      }

 // General case (U::resource_t != R)
 template <typename U, typename R>
      typename U::value_t callCalcValue(U const& u, R const& r){
        // Fail immediately!
        assert(!"Unit resource does not match");
        return U::value_t();
      }

int main()
{
    // Create an array of unit variants
   typedef unit_t<resource1_t,int> U1;
   typedef unit_t<resource2_t,float> U2;
   std::vector<std::variant<U1,U2>> units;
   units.emplace_back(U1(1));
   units.emplace_back(U2(1.0f));

   // Create a parallel array of resources
   std::vector<std::variant<resource1_t,resource2_t>> resources;
   resources.emplace_back(resource1_t());
   resources.emplace_back(resource2_t());

   // Call calcValue for each unit on the parallel resource
   for(int i(0); i<units.size(); ++i){
       std::visit([&](auto&& unit){
           std::visit([&](auto&& resource){
             // Fails to compile with substitution failure...
             //std::cout << unit.calcValue(resource) << "\n";

             // Results in ambiguous call compile error...
             std::cout << callCalcValue(unit,resource) << "\n";
           },resources[i]);
       },units[i]);
   }
}

I expected the compiler to match all cases where std::is_same_v<U::resource_t,R> to the specific case and all other combinations to the general case, instead, the compiler fails saying the function is ambiguous. I also tried ! std::is_same for the second definition and the compiler fails with error: redefinition of ... callCalcValue()...

Upvotes: 1

Views: 1253

Answers (2)

p-a-o-l-o
p-a-o-l-o

Reputation: 10057

I wonder if a simple overload could work as well:

// General case (U::resource_t != R)
template <typename U, typename R>
     typename U::value_t callCalcValue(U const& u, R const& r){
       // Fail immediately!
       assert(!"Unit resource does not match");
       return U::value_t();
     }

//Specific case (U::resource_t == R) overloads the general case
template <typename U>
      typename U::value_t callCalcValue(U const& u,  typename U::resource_t const& r){
        return u.calcValue(r);
      }

Upvotes: 1

Barry
Barry

Reputation: 303117

Here's a way reduced example:

template <typename T, typename R, typename=std::enable_if_t<std::is_same_v<R, int>>
void foo(T, R); // #1

template <typename T, typename R>
void foo(T, R); // #2

foo(some_t{}, some_r{});

It doesn't really matter what the particular sfinae constraint is, so I picked a simple one. Now, if the constraint (in this case is_same_v<R, int>) is not met, substitution into #1 fails and we're left with one single candidate: #2.

But if substitution succeeds, then we have two candidates. They're equivalently viable, there's nothing to distinguish them. Hence, the call is ambiguous!


We need an additional way to distinguish them. One way is to add the negated constrained to the other overload (note that you need to change the SFINAE form here to make this work):

template <typename T, typename R, std::enable_if_t<std::is_same_v<R, int>, int> = 0>
void foo(T, R); // #1

template <typename T, typename R, std::enable_if_t<!std::is_same_v<R, int>, int> = 0>
void foo(T, R); // #2

This ensures exactly one of the two is viable.

Another way would be to forward the trait to a separate overload set:

template <typename T, typename R>
void foo_impl(T, R, std::true_type); // #1

template <typename T, typename R>
void foo_impl(T, R, std::false_type); // #2

template <typename T, typename R>
void foo(T t, R r) {
    return foo_impl(t, r, std::is_same<R, int>{}); // NB: not _v
}

Another way would be to just write one overload and use if constexpr internally:

template <typename T, typename R>
void foo(T, R) {
    if constexpr (std::is_same_v<R, int>) {
        // #1
    } else {
        // #2
    }
}

A future way (in C++20) would be to use concepts:

template <typename T, typename R>
    requires std::is_same_v<R, int>
void foo(T, R); // #1

template <typename T, typename R>
void foo(T, R); // #2

This would make #1 "more constrained than" #2 if it's viable, so it would be preferred.

Upvotes: 5

Related Questions