Двухфазный поиск имён (two-stage name lookup) | Паршин Павел

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

#include <iostream>
#include <string>

std::string foo() noexcept {
    return "foo_free_function";
}

template<typename T>
class Base {
    public:
    std::string foo() const noexcept {
        return "foo_Base_function";
    }
};

template<typename T>
class Derived final : public Base<T> {
    public:
    std::string bar() const noexcept {
        return foo();
    }
};

int main() {
    Derived<int> d;
    std::cout << d.bar() << std::endl;
    return 0;
}

Как вы думаете, каков результат выполнения данного кода?

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

Например, при использовании GCC-5.1 вывод будет таким: foo_free_function.

При использовании VC++: foo_Base_function.

Если же убрать свободную функцию foo(), то GCC выдаст ошибку компиляции:

prog.cpp: In member function 'std::string Derived<T>::bar() const':
prog.cpp:20:14: error: there are no arguments to 'foo' that depend on a template parameter, so a declaration of 'foo' must be available [-fpermissive]
    return foo();
                ^
prog.cpp:20:14: note: (if you use '-fpermissive', G++ will accept your code, but allowing the use of an undeclared name is deprecated)

Давайте разберемся какой из компиляторов прав и почему это происходит.

Согласно стандарту C++ при разрешении имён в шаблонных определениях обычные правила поиска используются только для имён, используемых в независимом контексте (то есть если имя не зависит от аргумента шаблона). Поиск имён для шаблонных параметров откладывается до инстанцирования шаблона. Если имя не зависит от аргумента шаблона, определение для этого имени должно быть доступно в той области, где это имя появляется. В дальнейшем при инстанцировании шаблона найденное определение имени более не изменяется:

void f(char);

template void g(T t) { 
    f(1); // f(char)
    f(T(1)); // зависимое имя
    f(t); // зависимое имя
    dd++; // независимое имя. Ошибка компиляции: определение для dd не найдено. 
}

Это различие в поиске зависимых и независимых имён и получило название двухфазного (зависимого) поиска имён (two-stage lookup names).

В коде

std::string bar() const noexcept { 
    return foo();
}

функции bar() и foo() не зависят от аргумента шаблона, поэтому поиск определения функции foo() будет осуществляться не в родительском классе, а в окружающем пространстве имён (в данном случае это глобальное пространство). Как было сказано выше, при инстанцировании шаблона определение функции уже не поменяется. Для того чтобы явно указать необходимость поиска определения функции в родительском классе, можно либо использовать указатель this (который в данном примере будет иметь тип Derived<T>*, то есть добавляется зависимость от типа шаблона), либо использовать Base<T>::foo() или using Base<T>::foo().

Можно также использовать флаг -fpermissive, который сообщает компилятору, что поиск имён для всех функций, чьи определения недоступны на момент разбора шаблона, необходимо осуществлять позднее на этапе инстанцирования шаблона, как будто вызов функции является зависимым. Но использование данного флага не рекомендовано, поскольку тогда будет компилироваться заведомо невалидный код (с точки зрения стандарта) и он не позволяет обойти проблему при аналогичном использовании переменных, а не функций.

Некоторые компиляторы будут компилировать данный пример без ошибок, поскольку некоректно реализуют двухфазный поиск имён, что и было продемонстрировано на примере с использованием VC++.

Предыдущая запись Следующая запись