Вопрос или проблема
Следующий код дает разные результаты при компиляции с использованием gcc 14.2 или clang 19.1:
namespace mylib {
template <typename Type> void Function(const Type& t) {
// Примечание: явное указание Type::
t.Type::Foo();
}
}
class Type {
public:
void Foo() const {
std::cout << "Базовая функция";
}
};
class DerivedType: public Type {
public:
void Foo() const {
std::cout << "Производная функция";
}
};
int main() {
mylib::Function(DerivedType{});
}
- в gcc 14.2: вызывается DerivedType::Foo().
- в clang 19.1: вызывается Type::Foo().
Есть ли идеи о том, какое поведение следует считать правильным, если таковое существует?
Примечание: оба компилятора выдают одинаковый вывод, если class Type
переименовать в нечто иное, кроме аргумента шаблона Function
, в этом случае DerivedType::Foo
также вызывается clang.
Это (в настоящее время открытая) ошибка CWG 1089.
Согласно текущим правилам, поиск Type
выполняется при инстанцировании в контексте типа класса t
сначала, и шаблонный параметр с тем же именем может быть найден только во время неявного поиска после этой первой попытки, если Type
не найден в контексте класса.
Здесь Type
может быть найден в области класса DerivedType
типа t
, указывая на базовый класс, и Type::Foo
тогда будет ссылаться на член-функцию этого базового класса Type
.
Таким образом, здесь Clang прав.
Ответ или решение
Вопрос о различиях в области видимости квалифицированных имен членов в шаблонах между компиляторами GCC и Clang касается сложных правил C++ и их интерпретации различными компиляторами. Рассмотрим ваш пример кода подробнее:
namespace mylib {
template <typename Type> void Function(const Type& t) {
// Обратите внимание: явная квалификация Type::
t.Type::Foo();
}
}
class Type {
public:
void Foo() const {
std::cout << "Base function";
}
};
class DerivedType: public Type {
public:
void Foo() const {
std::cout << "Derived function";
}
};
int main() {
mylib::Function(DerivedType{});
}
Поведение компиляторов
При компиляции этого кода с использованием GCC 14.2, вызывается метод Foo
у класса DerivedType
. Однако при использовании Clang 19.1 вызывается метод Foo
у базового класса Type
. Это приводит к вопросу о корректности поведения этих компиляторов.
Правила поиска имени
Согласно текущим стандартам C++, при использовании квалифицированного имени происходит несколько этапов поиска:
-
Первичный поиск: Во время инстанциации шаблона, компилятор ищет имя
Type
в контексте класса объектаt
. Этот поиск включает в себя базовый класс, если типомt
является производный класс. То есть, компилятор сначала проверяет, доступно ли имяType
в контекстеDerivedType
, что приводит к обнаружениюType
. - Несквозной поиск: Если имя
Type
не найдено в контексте класса, компилятор выполняет несквозной поиск, в котором также могут быть учтены шаблонные параметры. Однако, в данном примере имяType
успешно обнаруживается в базовом классе, что должно приводить к ссылке наType::Foo()
.
Вывод
На основании анализа правил C++ можно заключить, что поведение Clang является корректным. Поиск имени Type
в контексте класса DerivedType
приводит к тому, что компилятор видит член Type::Foo
. Следовательно, вызов t.Type::Foo()
будет ссылаться на метод базового класса Type
, как это и происходит в Clang.
Также стоит подчеркнуть, что если имя класса Type
будет изменено на что-то отличное от имени шаблонного параметра, поведение будет единообразным для обоих компиляторов: DerivedType::Foo()
будет вызванным.
Заключение
Различия в поведении между GCC и Clang в данном примере подчеркивают важность понимания правил поиска имен в C++. На данный момент ясным представляется, что Clang следует существующим стандартам, а поведение GCC может быть пересмотрено в будущем. Это также поднимает вопрос о необходимости разъяснения и, возможно, изменения стандартов для обеспечения единообразия между компиляторами. Факт открытия CWG-дела № 1089 подтверждает, что это общепринятая проблема в сообществе разработчиков языков программирования.