Неоднозначность с помощью оператора [] и множественного наследования

Рассмотрим следующий класс:

class Foo
{
    public:

    void operator [] (const std::string& s) { }

    void operator [] (std::size_t idx) { }
};

Здесь, учитывая экземпляр Foo f, выражение f[0] не является двусмысленным, потому что компилятор выбирает вторую перегрузку. Аналогично, выражение f["abc"] не является двусмысленным, потому что компилятор выбирает первую перегрузку (так как a const char* преобразуется в std::string).

Итак, почему же тогда, что если у нас есть два базовых класса, каждый с другой перегрузкой, то возникает внезапная двусмысленность?

Предположим, что:

class Base1
{
    public:

    void operator [] (const std::string& s) { }
};

class Base2
{
    public:

    void operator [] (std::size_t idx) { }
};

class Derived : public Base1, public Base2
{ };

Теперь, если мы скажем:

Derived d;
d[0];

Компилятор жалуется:

    error: request for member ‘operator[]’ is ambiguous
      d[0];
         ^
   note: candidates are: void Base2::operator[](std::size_t)
      void operator [] (std::size_t idx) { }
           ^
   note:                 void Base1::operator[](const string&)
      void operator [] (const std::string& s) { }

Почему факт, что обе перегрузки операторов теперь находятся в базовых классах, вызывает какую-либо двусмысленность? И есть ли способ решить это?

EDIT: может ли это быть ошибкой компилятора (я использую GCC 4.8.1)

Ответ 1

Это не проблема с разрешением перегрузки, а скорее с поиском имени члена, который определен в 10.2. Подумайте (как бы я лучше не писал operator[] всюду):

struct base1 { void f(int); };
struct base2 { void f(double); };
struct derived : base1, base2 {};
int main() {
   derived d; d.f(0);
}

Когда поиск в f начинается в постфиксном выражении d.f(0), он сначала рассмотрит derived и обнаружит, что f не разрешает ничего вообще. 10.2/5 затем требует, чтобы поиск выполнялся параллельно со всеми базовыми классами, создавая отдельные наборы поиска. В этом случае S (f, base1) = {base1:: f} и S (f, base2) = {base2:: f}. Затем комплекты объединяются в соответствии с правилами в 10.2/6. Первая пуля касается слияния, когда один из наборов пуст, или если поиск разных наборов закончился одним и тем же членом (считайте, что он попал в общую базу). Вторая пуля интересна, так как она здесь применяется

10.2/6 bullet 2

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

То есть S (f, base1) отличается от S (f, base2), поэтому S (f, производный) становится недопустимым объявлением. И поиск не работает.

Ответ 2

Вызов неоднозначен, потому что два оператора не перегружают. Перегрузка применяется только к именам, которые определены в той же области. Base1 и Base2 определяют две области разные, поэтому в производном классе компилятор просто видит два одинаковых имени, у которых нет соединения. Как говорили другие ответы, способ преодолеть это - поднять оба имени в производный класс с соответствующими объявлениями using; когда это будет сделано, компилятор видит два имени в области определения производного класса и применяет разрешение перегрузки.

Ответ 3

class Derived : public Base1, public Base2
{ 
    public:
    using Base1::operator[];
    using Base2::operator[];
};

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

Ответ 4

TL; DR: хотя обе функции находятся в наборе кандидатов, набор кандидатов также недействителен, что делает программу плохо сформированной. Подробнее см. dribeas.


Обе функции явно жизнеспособны, поскольку:

f((size_t)0)

и

f((const char*)0)

являются законными, и обе последовательности преобразования неявны.

Первоначально, два кандидата не были двусмысленными, потому что один был лучшим преобразованием, чем другой. Компилятор выбрал тот, который требовал только интегральной рекламы. Поскольку интегральная акция была "лучше", чем другая последовательность конверсий, она выиграла.

Теперь, для обоих кандидатов требуется стрелка вверх. Теперь последовательность конверсий, включающая ускоренную и интегральную рекламу, уже не намного лучше. Поэтому компилятор не может выбирать, и он сообщает о двусмысленности. (Примечание. Я думаю, что последовательность преобразований без пользовательского преобразования все равно будет лучше, и кандидат f(Base2* implicit, size_t) должен все равно выиграть... но теперь это намного сложнее из-за правил разрешения перегрузки, связанных с преобразованиями нескольких аргументов. )

Объявление "using" позволяет передать указатель this с преобразованием идентичности вместо повышения, поэтому снова одна последовательность преобразований является всего лишь интегральной рекламой, которая лучше.


Из раздела 13.3.1:

Набор функций-кандидатов может содержать как функции-члены, так и не-члены, которые должны быть разрешены в отношении одного и того же списка аргументов. Таким образом, список аргументов и параметров сопоставим в этом гетерогенном наборе, считается, что функция-член имеет дополнительный параметр, называемый неявным параметром объекта, который представляет объект, для которого была вызвана функция-член. Для целей разрешения перегрузки как статические, так и нестатические функции-члены имеют неявный параметр объекта, но конструкторы этого не делают.

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

и

Во время разрешения перегрузки подразумеваемый аргумент объекта неотличим от других аргументов. Однако параметр неявного объекта сохраняет свою идентичность, поскольку преобразования в соответствующем аргументе должны подчиняться этим дополнительным правилам:

  • не может быть введен временный объект для хранения аргумента для параметра неявного объекта; и

  • никакие пользовательские преобразования не могут применяться для достижения соответствия типа с ним.

Ответ 5

Вы пытались явно сказать, что Derived предоставляет и то, и другое?

class Derived : public Base1, public Base2
{
public:
    using Base1::operator[];
    using Base2::operator[];
};

Я не знаю, может ли это работать, у меня есть только Visual.