Подтвердить что ты не робот

Инфикс против синтаксиса префикса: различия в поиске имен

Операторы в С++ обычно рассматриваются как альтернативный синтаксис для функций/методов, особенно в контексте перегрузки. Если это так, два приведенных ниже выражения должны быть синонимичными:

std::cout << 42;
operator<<(std::cout, 42);

На практике второе утверждение приводит к следующей ошибке:

call of overloaded ‘operator<<(std::ostream&, int)’ is ambiguous

Как обычно, такое сообщение об ошибке сопровождается списком возможных кандидатов:

operator<<(basic_ostream<_CharT, _Traits>& __out, char __c)
operator<<(basic_ostream<char, _Traits>& __out, char __c)
operator<<(basic_ostream<char, _Traits>& __out, signed char __c)
operator<<(basic_ostream<char, _Traits>& __out, unsigned char __c)

Такая ошибка вызывает как минимум два вопроса:

  • Каким образом эти два оператора отличаются (в терминах поиска по имени)?
  • Почему operator<<(basic_ostream<char, _Traits>& __out, int __c) отсутствует?

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

4b9b3361

Ответ 1

Нет, эти два выражения не должны быть синонимами. std::cout << 42 отображается как как operator<<(std::cout, 42), так и std::cout.operator<<(42). Оба поиска производят жизнеспособных кандидатов, но второй - лучшее совпадение.

Ответ 2

Это правила поиска операторов из С++ 17 [over.match.oper/3], где я отредактировал для краткости, удалив текст, не относящийся к перегрузке operator<< с левым операндом, являющимся типом класса; и выделил раздел, который я объясню позже:

Для бинарного оператора @ с левым операндом типа, чья cv-безусловная версия - T1 и правым операндом типа чья cv-безоговорочная версия - T2, три набора функций-кандидатов, назначенные кандидаты в члены, кандидаты, не являющиеся членами, и встроенные -в кандидатах, построены следующим образом:

  • Если T1 является полным типом класса или классом, определяемым в настоящее время, набор кандидатов в члены является результатом квалифицированного поиска T1::[email protected] (16.3.1.1.1); в противном случае набор кандидатов в члены пуст.
  • Набор кандидатов, не являющихся членами, является результатом неквалифицированного поиска [email protected] в контексте выражения в соответствии с обычными правилами для поиска имени в неквалифицированных вызовах функций, за исключением того, что все функции-члены игнорируются.

Встроенные кандидаты здесь пусты, что относится к функциям поиска, которые неявно преобразуют оба операнда в целочисленные типы и применяют оператор сдвига битов; но не существует неявного преобразования из iostreams в целочисленный тип.

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


В чем причина того, что поиск операторов отличается от поиска других функций, и что все это значит? Я думаю, что лучше всего ответить на пару примеров. Во-первых:

struct X{ operator int(); };

void f(X);

struct A
{
    void f(int);

    void g() { X x; f(x); }    // Calls A::f
};

В этом примере есть принцип: если вы пытаетесь вызвать функцию-член класса из другой функции-члена класса; он определенно должен найти эту функцию-член, и не должен иметь поиск, загрязненный внешними функциями (даже включая ADL).

Таким образом, часть безусловных правил поиска состоит в том, что если не-ADL часть поиска находит функцию-член класса, то ADL не выполняется.

Без этого правила f(x) найдет и A::f и ::f а затем разрешение перегрузки выберет ::f качестве лучшего соответствия, чего мы не хотим.

На второй пример:

struct X{};
std::ostream& operator<<(std::ostream&, X);

struct S
{
    std::ostream& operator<<(int);

    void f()
    {
         X x;
         std::cout << x;   // OK
         // operator<<(std::cout, x);  // FAIL
         // std::cout.operator<<(x);   // FAIL
    }
};

По принципу предыдущего примера - если бы правила были просто std::cout << 42; преобразовывается в operator<<(std::cout, 24); тогда поиск имени найдет S::operator<< и остановится. Упс!

Так что я думаю, что не совсем правильно говорить, что поведение строки OK выше происходит из-за выполнения обеих строк, помеченных как FAIL, как предлагали другие ответы/комментарии.


РЕЗЮМЕ:

Теперь мы можем понять фактическую формулировку стандартной цитаты в верхней части моего ответа.

Код std::cout << x; будут:

  • Посмотрите как std::cout.operator<<(x); А ТАКЖЕ
  • Посмотрите как operator<<(std::cout, x) ЗА ИСКЛЮЧЕНИЕМ ТОГО, ЧТО НЕ УКАЗАНЫ функции-члены (и, следовательно, ADL-подавление отсутствует из-за того, что функция-член найдена).

Затем выполняется разрешение перегрузки для объединения этих двух наборов.