двусмислен оператор [] в променлив шаблон

Опитвам се да компилирам този пример, където променлив шаблон на клас наследява от променливо количество бази, всяка от които имплементира различен 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 Мисля, че clang bug.   -  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
Не виждам как това отговаря на въпроса tbh - person Lightness Races in Orbit; 21.07.2015
comment
УАУ ... това е работа, вероятно ще ми отнеме известно време, за да разбера напълно вашия пример (все още не съм напълно опитен в C++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, три набора кандидат функции, обозначени като член кандидати, нечленуващи кандидати и вградени кандидати, се конструират както следва:
— Ако 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-declarations, които въвеждат всички членски функции на базовия клас в производния клас, така че първоначалният набор за търсене да намира всичко.

За да съкратите примера си:

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 има usings и няма грешки. 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