Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When overloading a function with multiple inheritance, GCC says calling it is ambiguous, but Clang and MSVC do not

I am using this variant library: https://github.com/cbeck88/strict-variant. It provides a class similar to std::variant and boost::variant. Given this struct:

struct S
{
    explicit S(double) {}
};

I want to do this:

strict_variant::variant<double, S> v = 2.0;

This works with Clang 5.0.1 and MSVC 19.12.25831.00, but fails to compile with GCC 7.2.1.

I looked at the library's code and reduced the problem to this:

#include <iostream>

struct S
{
    constexpr S() {}
    constexpr explicit S(double) {}
};

template<unsigned i> struct init_helper;
template<> struct init_helper<0> { using type = double; };
template<> struct init_helper<1> { using type = S; };

template<unsigned i>
struct initializer_leaf
{
    using target_type = typename init_helper<i>::type;
    constexpr unsigned operator()(target_type) const
    {
        return i;
    }
};

struct initializer : initializer_leaf<0>, initializer_leaf<1>
{
};

int main()
{
    std::cout << initializer()(double{}) << " = double" << '\n';
    std::cout << initializer()(S{}) << " = S" << '\n';

    return 0;
}

with the output being

0 = double
1 = S

GCC says:

strict_variant_test.cpp: In function ‘int main()’:
strict_variant_test.cpp:29:37: error: request for member ‘operator()’ is ambiguous
  std::cout << initializer()(double{}) << " = double" << '\n';
                                     ^
strict_variant_test.cpp:17:21: note: candidates are: constexpr unsigned int initializer_leaf<i>::operator()(initializer_leaf<i>::target_type) const [with unsigned int i = 1; initializer_leaf<i>::target_type = S]
  constexpr unsigned operator()(target_type) const
                     ^~~~~~~~
strict_variant_test.cpp:17:21: note:                 constexpr unsigned int initializer_leaf<i>::operator()(initializer_leaf<i>::target_type) const [with unsigned int i = 0; initializer_leaf<i>::target_type = double]
strict_variant_test.cpp:30:32: error: request for member ‘operator()’ is ambiguous
  std::cout << initializer()(S{}) << " = S" << '\n';
                                ^
strict_variant_test.cpp:17:21: note: candidates are: constexpr unsigned int initializer_leaf<i>::operator()(initializer_leaf<i>::target_type) const [with unsigned int i = 1; initializer_leaf<i>::target_type = S]
  constexpr unsigned operator()(target_type) const
                     ^~~~~~~~
strict_variant_test.cpp:17:21: note:                 constexpr unsigned int initializer_leaf<i>::operator()(initializer_leaf<i>::target_type) const [with unsigned int i = 0; initializer_leaf<i>::target_type = double]

But, it works with GCC (and still Clang and MSVC) when I change the definition of initializer to this:

struct initializer
{
    constexpr unsigned operator()(double) const
    {
        return 0;
    }

    constexpr unsigned operator()(S) const
    {
        return 1;
    }
};

My understanding of C++ says that this is equivalent, so I assume that this is a bug in GCC, but I have often run into problems where the standard says surprising things and my assumption is wrong. So, my question is: whose fault is this? Does GCC have a bug, do Clang and MSVC have a bug, or is the interpretation of the code undefined/unspecified such that all compilers are right? If the code is wrong, how can it be fixed?

like image 707
Mr. Metric Avatar asked Dec 22 '17 03:12

Mr. Metric


1 Answers

This is actually a clang bug.

The rule of thumb is that the names in different scopes don't overload. Here's a reduced example:

template <typename T>
class Base {
public:
    void foo(T ) { }
};

template <typename... Ts>
struct Derived: Base<Ts>...
{};

int main()
{
    Derived<int, double>().foo(0); // error
}

This should be an error because the class member lookup rules state that basically only one base class can contain a given name. If more than one base class has the same name, lookup is ambiguous. The resolution here is to bring both base class names into the derived class with a using-declaration. In C++17, that using declaration can be a pack expansion, which makes this problem a whole lot easier:

template <typename T>
class Base {
public:
    void foo(T ) { }
};

template <typename... Ts>
struct Derived: Base<Ts>...
{
    using Base<Ts>::foo...;
};

int main()
{
    Derived<int, double>().foo(0); // ok! calls Base<int>::foo
}

For the specific library, this code:

template <typename T, unsigned... us>
  struct initializer_base<T, mpl::ulist<us...>> : initializer_leaf<T, us>... {
      static_assert(sizeof...(us) > 0, "All value types were inelligible!");
  };

should look like:

template <typename T, unsigned... us>
struct initializer_base<T, mpl::ulist<us...>> : initializer_leaf<T, us>... {
    static_assert(sizeof...(us) > 0, "All value types were inelligible!");
    using initializer_leaf<T, us>::operator()...; // (*) <==
};

(although I guess the library is targeting C++11, so I submitted a C++11-compliant fix for it... which is just a bit more verbose).

like image 61
Barry Avatar answered Oct 14 '22 17:10

Barry