Перегрузка функций и область видимости

123
18 февраля 2021, 23:40

Исходный пример:

#include <ostream>
template<typename T>
void serialize(std::ostream& stream, const T& value) {}
template<typename T>
class Field
{
public:
    void serialize(std::ostream& stream)
    {
        serialize(stream, m_value);
    }
protected:
    T m_value = {};
};

Пример кода не компилируется (gcc-9, std=c++2a). Однако вывод компилятора подсказывает, что функция serialize(std::ostream&, const T&) недоступна для вызова из данного контекста. Меняем тело метода serialize на следующий код:

::serialize(stream, m_value);

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

P.S. Проверил с non-template классом - ситуация аналогичная.

Answer 1

Сразу надо заметить, что немедленной ошибки в приведенном вами коде нет, ибо приведенный шаблонный код не инстанцирован и name lookup для него сразу не выполняется, ибо внутренний вызов serialize зависит от шаблонного параметра T. И при конкретном инстанцировании результаты процесса name lookup для имени serailize теоретически могут зависеть и от конкретного типа T (см. ниже), и от того места, в котором выполнено инстанцирование. В вашем примере ничего этого не приведено.

Но с общей точки зрения вы наблюдаете классический name hiding. Любое имя во вложенной области видимости скрывает все имена из охватывающих областей видимости.

В явном виде это явление в стандарте не описано (или может даже описано, как вы сами сказали в [basic.scope.hiding]), но в любом случае оно вытекает напрямую из того факта, что name lookup и overload resolution - два отдельных этапа в С++. Сначала делается unqualified name lookup, который традиционно выполняет поиск изнутри-наружу снизу-вверх и останавливается на той области видимости, к которой найдено хотя бы одно подходящее имя. Затем запускается процесс overload resolution только для имен, найденных именно в этой области видимости.

Тут можно было бы понадеяться на argument-dependent lookup (ADL), который при правильном типе аргумента m_value мог бы увидеть и глобальное объявление serialize. Однако стандарт явно говорит, что если обычный name lookup нашел в качестве кандидата имя члена класса, то ADL тут же игнорируется (http://eel.is/c++draft/basic.lookup.argdep#3.1). Поэтому в вашем случае ADL не помогает.

В нижеприведенном же примере тоже имеет место name hiding, но сокрытие выполняется не методом класса, а внешней функцией

#include <ostream>
#include <iostream>
struct S {};
template<typename T>
void serialize(std::ostream& stream, const T& value) {}
namespace X {
  void serialize(std::ostream& stream) {}
  template<typename T>
  class Field
  {
  public:
    void foo(std::ostream& stream)
    {
      serialize(stream, m_value);
    }
  protected:
    T m_value = {};
  };
}
int main()
{
  X::Field<int> f1;
  f1.foo(std::cout); // <- ошибка 
  X::Field<S> f2;
  f2.foo(std::cout); // <- все в порядке
}

В таком варианте ADL не игнорируется. В этом примере при T == int ADL ничего не находит и мы получаем ту же самую ошибку по той же самой причине, что и в вашем примере. А вот при T == S ADL позволяет нам найти еще и глобальную serialize и ошибка пропадет.

READ ALSO
Программа работает медленне, чем должна

Программа работает медленне, чем должна

Есть код простой небольшой демки на C++В ней реализовывается механика "ходить, ставить блоки, убирать блоки"

116
Странное поведение QString (проблема с кодировкой)

Странное поведение QString (проблема с кодировкой)

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

123
Вопрос по полиморфизму

Вопрос по полиморфизму

Как объекту класса kvadrat вызвать методы класса rectangle, а потом опять вызывать методы kvadrat??

108