ambiguous operator[] in variadic template

2019-05-08 06:39发布

I'm trying to compile this example, where a variadic class template inherits from a variadic amount of bases, each of which implements a different operator[]:

#include <iostream>

template <typename T>
struct Field {
  typename T::value_type storage;

  typename T::value_type &operator[](const T &c) {
    return storage;
  }
};

template<typename... Fields>
struct ctmap : public Field<Fields>... {
};

int main() {
    struct age { typedef int value_type; };
    struct last_name { typedef std::string value_type; };

    ctmap<last_name, age> person;

    person[last_name()] = "Smith";
    person[age()] = 104;
    std::cout << "Hello World!" << std::endl;
    return 0;
}

When I compile with gcc (Debian 4.9.2-10), I get the following error

main.cpp: In function ‘int main()’:
main.cpp:22:23: error: request for member ‘operator[]’ is ambiguous
     person[last_name()] = "Smith";
                       ^
main.cpp:7:27: note: candidates are: typename T::value_type& Field<T>::operator[](const T&) [with T = main()::age; typename T::value_type = int]
   typename T::value_type &operator[](const T &c) {
                           ^
main.cpp:7:27: note:                 typename T::value_type& Field<T>::operator[](const T&) [with T = main()::last_name; typename T::value_type = std::basic_string<char>]
main.cpp:23:17: error: request for member ‘operator[]’ is ambiguous
     person[age()] = 104;
                 ^
main.cpp:7:27: note: candidates are: typename T::value_type& Field<T>::operator[](const T&) [with T = main()::age; typename T::value_type = int]
   typename T::value_type &operator[](const T &c) {
                           ^
main.cpp:7:27: note:                 typename T::value_type& Field<T>::operator[](const T&) [with T = main()::last_name; typename T::value_type = std::basic_string<char>]

Why is this ambiguous?

2条回答
老娘就宠你
2楼-- · 2019-05-08 07:20

The code is invalid and gcc is correct to reject it (clang 3.6.0 accepts it though - this is a bug). The rules for looking up an operator start with, from [over.match.oper]:

[...] for a binary operator @ with a left operand of a type whose cv-unqualified version is T1 and a right operand of a type whose cv-unqualified version is T2, three sets of candidate functions, designated member candidates, non-member candidates and built-in candidates, are constructed as follows:
— If T1 is a complete class type or a class currently being defined, the set of member candidates is the result of the qualified lookup of T1::operator@ (13.3.1.1.1); otherwise, the set of member candidates is empty.

The lookup rules for a member name are (since we're looking up ctmap<last_name,age>::operator[]), from [class.member.lookup]:

The lookup set for f in C, called S(f,C), [...] is calculated as follows:

If C contains a declaration of the name f, [...]

Otherwise (i.e., C does not contain a declaration of f or the resulting declaration set is empty), S(f,C) is initially empty. If C has base classes, calculate the lookup set for f in each direct base class subobject Bi, and merge each such lookup set S(f,Bi) in turn into S(f,C).

The following steps define the result of merging lookup set S(f,Bi) into the intermediate S(f,C):
— [...]
— Otherwise, if the declaration sets of S(f,Bi) and S(f,C) differ, the merge is ambiguous: the new S(f,C) is a lookup set with an invalid declaration set and the union of the subobject sets. In subsequent merges, an invalid declaration set is considered different from any other.
— [...]

Basically - we have two base classes here, both with an operator[]. Both declaration sets differ - so the merge is ambiguous. The way to disambiguate would be to introduce using-declarations bringing in all the base class member functions into the derived class, so that the initial lookup set finds everything.

To shorten your example:

struct A { void foo(char) { } };
struct B { void foo(int ) { } };

struct C : A, B { };

struct D : A, B {
    using A::foo;
    using B::foo;
};

With that hierarchy

C c;
c.foo(4);  // error: ambiguous lookup set for foo()

D d;
d.foo('x') // OK: calls A::foo()
查看更多
Animai°情兽
3楼-- · 2019-05-08 07:23

A portable way do do what you want is roughly:

template<class...Ts>
struct operator_index_inherit {};
template<class T0, class T1, class...Ts>
struct operator_index_inherit<T0, T1, Ts...>:
  T0, operator_index_inherit<T1, Ts...>
{
  using T0::operator[];
  using operator_index_inherit<T1, Ts...>::operator[];
};
template<class T0>
struct operator_index_inherit<T0>:
  T0
{
  using T0::operator[];
};

then:

template<class... Fields>
struct ctmap : operator_index_inherit<Field<Fields>...> {
  using base = operator_index_inherit<Field<Fields>...>;
  using base::operator[];
};

here we linearly inherit from each of the types, and using operator[] on our parents.

If we could using Field<Fields>::operator[]...; we would not have to do this.

Some care has to be taken with constructors (which I did not take), but you might not need to do this.

live example.


What is actually going wrong depends on details of the standard I am less than certain of. Basically, you are mixing operators and inheritance and overloading in a complex way. Even if your code is standard compliant (which it may or may not be), it is compliant in a way that some compilers die on.

查看更多
登录 后发表回答