Search code examples
c++c++17variadic-templatestemplate-meta-programmingtemplate-argument-deduction

Why is it an ambigious function call using GCC? Template deduction failing?


I am unable to compile my current, in my opinion, valid C++(17) code with either GCC nor clang.

I recently managed to cause a bug compiling my (in my opinion) valid C++17 code with clang (Bug report: https://bugs.llvm.org/show_bug.cgi?id=40305). Later on I changed the code and got an error trying to compile the code with GCC as well.

I managed to isolate the problematic code parts and found possible work-arounds for both compilers, which also work in my real code:

#include <iostream>
#include <utility>

template<class T, int... Ns>
class Tensor
{
};

template<class T, int... Ns>
class Base
{
};

template<class T, int... Ns>
class Derived : public Base<T, Ns...>
{
};

template<class T, int... Ns>
decltype(auto) convert(Base<T, Ns...> const &a)
{
    return a;
}

template<class T, int... Ns>
auto convert(Tensor<T, Ns...> const &)
{
    return Derived<T, Ns...>();
}

#ifdef WGCC1 // First work-around for GCC
template<class T, int... Ns>
void error(Base<T, Ns...> const &arg)
{
    std::cout << "Function" << std::endl;
}
#endif

template<class... Ts, int... Ns>
void error(Base<Ts, Ns...> const &... args)
{
    std::cout << "Function" << std::endl;
}

template<class... Ts
#ifdef WGCC2 // Second work-around for GCC
    >
#else
    ,
    class = std::enable_if_t<
        std::conjunction<std::is_class<std::remove_reference_t<Ts>>...>{}>>
#endif
void error(Ts &&... args)
{
    std::cout << "Wrapper: ";
    ((std::cout << typeid(args).name() << " "), ...);
    std::cout << std::endl;
#ifdef WCLANG // Work-around for clang, see:
              // https://bugs.llvm.org/show_bug.cgi?id=40305
    return error(convert(convert(std::forward<Ts>(args)))...);
#else
    return error(convert(std::forward<Ts>(args))...);
#endif
}

int main()
{
    Tensor<int, 4, 4> a;
    error(a);
}

See: https://godbolt.org/z/L5XVgL

Note: Obviously this code doesn't make sense anymore, i.e. SFINAE checking for std::is_class. However the problem is the same as in my, meaningful, real code.

Results:

  • clang without work-around causes an internal compiler error.
  • GCC without any work-around results in an ambiguous function call to
error(const Base<T, Ns...>&)

I actually expect no work-around to be required. clang shall be able to deduce the template parameters. I can't see how GCC comes up with the idea that the function call is ambiguous, in my opinion it is very clear. Am I doing something wrong or assuming template deductions to work that shall not work according to the C++(17) standard?

Other unexpected behavior: With one of the GCC work-arounds enabled, the wrapper function is called twice. I'm not sure this is expected. Or in other words: I'm not sure whether template deduction or conversion to base is done first. Clang seems to be of different opinion here than GCC. I.e. conversion to reference to base is done first, however afterwards template deduction fails (see bug report). Which one is right?

Update: Filed bug for GCC as well: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88907


Solution

    • Let simplify the problem for gcc to:

      template<class... Ts, int... Ns>
      void error(Base<Ts, Ns...> const &... args) {} // #1
      
      template<class... Ts,
          std::enable_if_t<
              std::conjunction<std::is_class<std::remove_reference_t<Ts>>...>{}, int> = 0>
      void error(Ts &&... args) {} #2
      
      int main()
      {
          const Base<int, 4> b;
      
          error(b); // call #1
      }
      

      Following the (complicated) rules of overload_resolution

      we have both methods as viable functions, both are template, so we use the more specialized template

      I (as clang) understand that #1 is more specialized than #2, but not for gcc

      Demo

      I would say gcc bug.

    • Let simplify the problem for clang to:

      template<class... Ts, int... Ns>
      void error(Base<Ts, Ns...> const &... args) {} // #1
      
      template<class... Ts,
          std::enable_if_t<
              (... && std::is_class<std::remove_reference_t<Ts>>::value), int> = 0>
      void error(Ts&&... args) {} // #2
      
      int main()
      {
          Derived<int, 4, 4> d;
          error(d); // call #2
      }
      

      ICE is always a bug, so bug of clang.

      For resolution, #2 is an exact match, whereas #1 requires conversion of a derived class to its base.

      gcc agrees that #2 is called.

      Demo