GCC не может различить operator++() и operator++ (int)

template <typename CRTP>
struct Pre {
    CRTP & operator++();
};

template <typename CRTP>
struct Post {
    CRTP operator++(int);
};

struct Derived
    : Pre<Derived>
    , Post<Derived>
{};

int main() {
    Derived d;
    d++;
    ++d;
}

Я получаю эти ошибки от GCC:

<source>: In function 'int main()':
<source>:18:10: error: request for member 'operator++' is ambiguous
        d++;
        ^~
<source>:8:14: note: candidates are: CRTP Post<CRTP>::operator++(int) [with CRTP = Derived]
        CRTP operator++(int);
            ^~~~~~~~
<source>:3:16: note:                 CRTP& Pre<CRTP>::operator++() [with CRTP = Derived]
        CRTP & operator++();
                ^~~~~~~~
<source>:19:11: error: request for member 'operator++' is ambiguous
        ++d;
        ^
<source>:8:14: note: candidates are: CRTP Post<CRTP>::operator++(int) [with CRTP = Derived]
        CRTP operator++(int);
            ^~~~~~~~
<source>:3:16: note:                 CRTP& Pre<CRTP>::operator++() [with CRTP = Derived]
        CRTP & operator++();
                ^~~~~~~~

Операторы пре-декремента и пост-декремента вызывают аналогичные ошибки. Нет таких ошибок с Clang. Есть идеи, что может быть не так или как обойти это?

Ответ 1

Поиск имени должен произойти первым. В данном случае для имени operator++.

[basic.lookup] (акцент мой)

1 Правила поиска имен применяются одинаково ко всем именам (включая typedef-names ([dcl.typedef]), namespace-names ([basic.namespace]) и class-names ([class.name])) везде, где позволяет грамматика такие имена в контексте обсуждаются по определенному правилу. Поиск имени связывает использование имени с объявлением ([basic.def]) этого имени. Поиск имени должен найти однозначное объявление для имени (см. [Class.member.lookup]). Поиск имени может связывать более одного объявления с именем, если он находит имя как имя функции; говорят, что объявления формируют набор перегруженных функций ([over.load]). Разрешение перегрузки ([over.match]) происходит после успешного поиска имени. Правила доступа (пункт [class.access]) рассматриваются только после успешного поиска имени и разрешения перегрузки функции (если применимо). Только после поиска имени, разрешения перегрузки функции (если применимо) и проверки доступа успешно выполняются атрибуты, представленные объявлением имени, которые используются в дальнейшем при обработке выражений (пункт [expr]).

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

[class.member.lookup]

8 Если имя перегруженной функции найдено однозначно, разрешение перегрузки ([over.match]) также имеет место перед контролем доступа. Неоднозначности часто могут быть решены путем присвоения имени его имени класса. [ Пример:

struct A {
  int f();
};

struct B {
  int f();
};

struct C : A, B {
  int f() { return A::f() + B::f(); }
};

- конец примера]

Пример в значительной степени суммирует довольно длинные правила поиска в предыдущих абзацах [class.member.lookup]. В вашем коде есть неоднозначность. GCC правильно сообщить об этом.


Что касается обхода этого, люди в комментариях уже представили идеи для обхода проблемы. Добавьте вспомогательный класс CRTP

template <class CRTP>
struct PrePost
    : Pre<CRTP>
    , Post<CRTP>
{
    using Pre<CRTP>::operator++;
    using Post<CRTP>::operator++;
};

struct Derived : PrePost<Derived> {};

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