4

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?

Barry
  • 286,269
  • 29
  • 621
  • 977
Trungus
  • 165
  • 8
  • 1
    This is accepted by clang++3.8: http://melpon.org/wandbox/permlink/huzwGp0kc2OafMZl (but I'm not sure which compiler is right in this case) – dyp Jul 20 '15 at 20:17
  • (Basically member functions in multiple base classes do not overload. I'm not sure how this interacts with operator lookup though; it seems at first glance that clang is wrong.) – dyp Jul 20 '15 at 20:40
  • You could make this compliant with a linear/tree based inheritance, and `using` declarations. – Yakk - Adam Nevraumont Jul 20 '15 at 20:43
  • is true dyp, I try this code in coliru with clang 3.6 and work fine, but I need to use gcc, in this case the version is 4.9.2. – Trungus Jul 20 '15 at 20:51
  • Yakk, can you explain me how can I do a linear/tree base inheritance?, can you give some example? – Trungus Jul 20 '15 at 20:52
  • @dyp I think clang bug. – Barry Jul 20 '15 at 21:10

2 Answers2

3

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
2

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()
Barry
  • 286,269
  • 29
  • 621
  • 977
  • I understand .... I have a question, How can I call B::foo()? ... I try to cast the "4", or declare a variable int but always give the ambiguous lookup. – Trungus Jul 20 '15 at 21:31
  • @Trungus The arguments don't matter - it's the actual lookup of the name `foo` that fails because it's ambiguous. To call it, you need to have the *using-declaration* (`using B::foo` in the class). Or you could call it through a pointer to member (`(C{}.*&B::foo)(4);` <== don't write this code, I'm just including it for completeness) – Barry Jul 20 '15 at 21:34
  • correct me if I wrong but your example already have the "using B:foo" in the class declaration so, why still have the ambiguous lookup error? – Trungus Jul 20 '15 at 21:41
  • @Trungus `D` has the `using`s, and has no errors. `C` omits them, and thus has errors. – Barry Jul 20 '15 at 21:43
  • How does this apply to operator lookup? `[]`, or even free binary `operator*`? The op is invoking an operator, not calling a method. It may be dispatched to the method operator, but does it follow the exact same rules? – Yakk - Adam Nevraumont Jul 20 '15 at 21:51
  • @Yakk It's still a name, no? – Barry Jul 20 '15 at 22:26
  • It is an operator. Operator lookup is a mixture of overload resolution and member resolution. Your argument is persuasive for member lookup: but [obviously operators are more than just member lookup](http://coliru.stacked-crooked.com/a/13bc82aee4eca662). – Yakk - Adam Nevraumont Jul 21 '15 at 00:14
  • [This one](http://coliru.stacked-crooked.com/a/1bcc1368ddf58488) seems to indicate it is when there are two different member `operator*`s things break, and not before. Which does make sense. But clang disagrees. – Yakk - Adam Nevraumont Jul 21 '15 at 00:21
  • 1
    @Yakk [over.match.oper] starts with building up the candidate set as "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." So we still follow the member lookup rules - but we just add the non-member ones that match as well. – Barry Jul 21 '15 at 10:25