неоднозначный оператор [] в вариативном шаблоне

Я пытаюсь скомпилировать этот пример, где шаблон вариативного класса наследуется от вариативного количества баз, каждая из которых реализует разные 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;
}

Когда я компилирую с gcc (Debian 4.9.2-10), я получаю следующую ошибку

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>]

Почему это двусмысленно?


person Trungus    schedule 20.07.2015    source источник
comment
Это принимает clang++3.8: melpon.org/wandbox/permlink/huzwGp0kc2OafMZl (но Я не уверен, какой компилятор прав в этом случае)   -  person dyp    schedule 20.07.2015
comment
(В основном функции-члены в нескольких базовых классах не перегружаются. Я не уверен, как это взаимодействует с поиском оператора; на первый взгляд кажется, что clang неверен.)   -  person dyp    schedule 20.07.2015
comment
Вы можете сделать это совместимым с линейным/древовидным наследованием и объявлениями using.   -  person Yakk - Adam Nevraumont    schedule 20.07.2015
comment
это правда dyp, я пробую этот код в coliru с clang 3.6 и работает нормально, но мне нужно использовать gcc, в данном случае версия 4.9.2.   -  person Trungus    schedule 20.07.2015
comment
Yakk, можете ли вы объяснить мне, как я могу сделать линейное/древовидное базовое наследование? Можете ли вы привести какой-нибудь пример?   -  person Trungus    schedule 20.07.2015
comment
@dyp Я думаю, ошибка лязга.   -  person Barry    schedule 21.07.2015


Ответы (2)


Портативный способ сделать то, что вы хотите, примерно:

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[];
};

тогда:

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

здесь мы линейно наследуем от каждого из типов и using operator[] от наших родителей.

Если бы мы могли using Field<Fields>::operator[]...;, нам не пришлось бы этого делать.

С конструкторами нужно быть осторожным (чего я не делал), но вам может и не понадобиться этого делать.

живой пример.


То, что на самом деле идет не так, зависит от деталей стандарта, в которых я менее чем уверен. По сути, вы смешиваете операторы, наследование и перегружаете сложным образом. Даже если ваш код соответствует стандарту (что может быть, а может и не быть), он соответствует требованиям, от которых умирают некоторые компиляторы.

person Yakk - Adam Nevraumont    schedule 20.07.2015
comment
Я не понимаю, как это отвечает на вопрос. - person Lightness Races in Orbit; 21.07.2015
comment
ВАУ... это работа, возможно, мне потребуется некоторое время, чтобы полностью понять ваш пример (я еще не полностью разбираюсь в С++ 11), большое спасибо - person Trungus; 21.07.2015
comment
Тогда это половина :) - person Lightness Races in Orbit; 21.07.2015

Код недействителен, и gcc правильно отклоняет его (хотя clang 3.6.0 его принимает - это ошибка). Правила поиска оператора начинаются с [over.match.oper]:

[...] для бинарного оператора @ с левым операндом типа, чья cv-неквалифицированная версия равна T1, и правым операндом типа, чья cv-неквалифицированная версия равна T2, три набора функций-кандидатов, обозначенных member кандидаты, кандидаты, не являющиеся членами и встроенные кандидаты, строятся следующим образом:
— если T1 является полным типом класса или классом, будучи определенным, набор кандидатов в члены является результатом квалифицированного поиска T1::operator@ (13.3.1.1.1); в противном случае набор кандидатов в члены пуст.

Правила поиска для имени члена (поскольку мы ищем ctmap<last_name,age>::operator[]) из [class.member.lookup]:

Набор поиска для f в C, называемый S(f,C), [...] вычисляется следующим образом:

Если C содержит объявление имени f, [...]

В противном случае (т.е. C не содержит объявления f или результирующий набор объявлений пуст), S(f,C) изначально пуст. Если у C есть базовые классы, вычислить набор поиска для f в каждом подобъекте прямого базового класса Bi и объединить каждый такой набор поиска S(f,Bi) в свою очередь с S(f,C).

Следующие шаги определяют результат слияния набора поиска S(f,Bi) с промежуточным набором S(f,C):
— [...]
— В противном случае, если наборы объявлений S(f,Bi) и S(f,C) различаются, слияние неоднозначно: новый S(f,C) является набором поиска с недопустимым набором объявлений и объединение множеств подобъектов. При последующих слияниях недопустимый набор объявлений считается отличным от любого другого.
— [...]

По сути, у нас есть два базовых класса, оба с расширением operator[]. Оба набора объявлений различаются, поэтому слияние неоднозначно. Чтобы устранить неоднозначность, можно было бы ввести using-declaration, включающие все функции-члены базового класса в производный класс, чтобы начальный поисковый набор находил все.

Чтобы сократить ваш пример:

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

struct C : A, B { };

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

С этой иерархией

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

D d;
d.foo('x') // OK: calls A::foo()
person Barry    schedule 20.07.2015
comment
Я понимаю.... У меня есть вопрос, как я могу вызвать B::foo()? ... Я пытаюсь привести 4 или объявить переменную int, но всегда даю неоднозначный поиск. - person Trungus; 21.07.2015
comment
@Trungus Аргументы не имеют значения - это фактический поиск имени foo, который терпит неудачу, потому что он неоднозначен. Чтобы вызвать его, вам нужно иметь using-declaration (using B::foo в классе). Или вы можете вызвать его через указатель на элемент ((C{}.*&B::foo)(4); ‹== не пишите этот код, я просто включаю его для полноты) - person Barry; 21.07.2015
comment
поправьте меня, если я ошибаюсь, но в вашем примере уже используется B:foo в объявлении класса, так почему же все еще возникает неоднозначная ошибка поиска? - person Trungus; 21.07.2015
comment
@Trungus D имеет using и не имеет ошибок. C пропускает их и, следовательно, содержит ошибки. - person Barry; 21.07.2015
comment
Как это относится к поиску оператора? [] или даже бесплатный бинарник operator*? Оператор вызывает оператор, а не метод. Он может быть отправлен оператору метода, но следует ли он точно таким же правилам? - person Yakk - Adam Nevraumont; 21.07.2015
comment
@Yakk Это все еще имя, не так ли? - person Barry; 21.07.2015
comment
Это оператор. Поиск оператора представляет собой смесь разрешения перегрузки и разрешения элементов. Ваш аргумент убедителен для поиска элементов: но очевидно, что операторы - это больше, чем просто поиск элементов. - person Yakk - Adam Nevraumont; 21.07.2015
comment
Этот указывает на то, что два разных члена operator* ломаются, и не раньше, чем. Что имеет смысл. Но Кланг не согласен. - person Yakk - Adam Nevraumont; 21.07.2015
comment
@Yakk [over.match.oper] начинается с создания набора кандидатов в виде Если T1 является полным типом класса или классом, который в настоящее время определяется, набор кандидатов-членов является результатом квалифицированного поиска T1::operator@ ( 13.3.1.1.1); в противном случае набор кандидатов в члены пуст. Таким образом, мы по-прежнему следуем правилам поиска членов, но мы просто добавляем те, которые не являются членами, которые также соответствуют. - person Barry; 21.07.2015