Коротка відповідь: щоб зробити x
залежне ім'я, щоб пошук був відкладений, поки не буде відомий параметр шаблону.
Довга відповідь: коли компілятор бачить шаблон, він повинен виконати певні перевірки негайно, не бачачи параметр шаблону. Інші відкладаються, поки не буде відомий параметр. Це називається двофазною компіляцією, і MSVC не робить цього, але це вимагає стандарт і реалізований іншими основними компіляторами. Якщо вам подобається, компілятор повинен скласти шаблон, як тільки він його побачить (до якогось внутрішнього представлення дерева розбору), і відкласти компіляцію екземпляра на потім.
Перевірки, які виконуються на самому шаблоні, а не на його конкретних моментах, вимагають, щоб компілятор міг вирішувати граматику коду в шаблоні.
Для C ++ (і C), щоб вирішити граматику коду, іноді потрібно знати, чи є тип чи ні. Наприклад:
#if WANT_POINTER
typedef int A;
#else
int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }
якщо A - тип, який оголошує покажчик (не має іншого ефекту, крім того, щоб затінити глобальний x
). Якщо A - це об'єкт, це множення (і заборона деяким операторам перевантажувати це незаконне, присвоюючи оцінку). Якщо вона неправильна, цю помилку потрібно діагностувати на фазі 1 , вона визначається стандартом як помилка в шаблоні , а не в якійсь конкретній інстанції. Навіть якщо шаблон ніколи не буде ініційованим, якщо A - це int
вищезгаданий код неправильно сформований і його потрібно діагностувати так, як це було б, якби foo
не шаблон взагалі, а звичайна функція.
Тепер стандарт говорить, що імена, які не залежать від параметрів шаблону, мають бути вирішеними на фазі 1. A
Тут не залежить залежне ім'я, воно стосується того ж самого, незалежно від типу T
. Отже, його потрібно визначити до того, як шаблон буде визначений для того, щоб його можна було знайти та перевірити у фазі 1.
T::A
було б ім'ям, яке залежить від Т. На фазі 1 ми, можливо, не можемо знати, що це тип чи ні. Тип, який в кінцевому підсумку буде використаний як T
інстанція, цілком ймовірно, ще не визначений, і навіть якщо це не ми знаємо, який тип (и) буде використовуватися як наш параметр шаблону. Але ми повинні вирішити граматику, щоб зробити наші дорогоцінні фази 1 перевірки на неправильно сформовані шаблони. Таким чином, у стандарті є правило для залежних імен - компілятор повинен вважати, що вони не типи, якщо не кваліфіковано, typename
щоб вказати, що вони є типами, або використовуються в певних однозначних контекстах. Наприклад, в template <typename T> struct Foo : T::A {};
, T::A
використовується як базовий клас і, отже, однозначно є типом. Якщо Foo
інстанціюється з деяким типом, який має член данихA
замість вкладеного типу A, це помилка в коді, що робить екземпляр (фаза 2), а не помилка в шаблоні (фаза 1).
А як щодо шаблону класу із залежним базовим класом?
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
Ім’я A залежне чи ні? З базовими класами будь-яке ім'я може з'являтися в базовому класі. Тож ми могли б сказати, що А - залежне ім'я, і трактувати його як нетиповий. Це призвело б до небажаного ефекту від того, що кожне ім'я в Foo є залежним, і тому кожен тип, що використовується в Foo (крім вбудованих типів), повинен бути кваліфікований. Всередині Foo вам доведеться написати:
typename std::string s = "hello, world";
тому що std::string
це буде залежною назвою, і, отже, вважається нетиповим, якщо не вказано інше. Ой!
Друга проблема із дозволом бажаного коду ( return x;
) полягає в тому, що навіть якщо Bar
він визначений раніше Foo
і x
не є членом у цьому визначенні, хтось може пізніше визначити спеціалізацію Bar
для певного типу Baz
, наприклад, у якого Bar<Baz>
є член даних x
, а потім інстанціювати Foo<Baz>
. Тож у цій інстанції ваш шаблон повертає дані даних, а не глобальний x
. Або, навпаки, якби базове визначення шаблону Bar
було x
, воно могло б визначити спеціалізацію без нього, і ваш шаблон буде шукати глобальний x
для повернення Foo<Baz>
. Я думаю, що це було визнано настільки ж дивовижним і неприємним, як проблема у вас, але це мовчки дивно, на відміну від кидання дивної помилки.
Щоб уникнути цих проблем, діючий стандарт говорить, що залежні базові класи шаблонів класів просто не враховуються для пошуку, якщо прямо не вимагати. Це зупиняє все від того, щоб бути залежним лише тому, що його можна було знайти в залежній базі. Це також має небажаний ефект, який ви бачите - ви повинні кваліфікувати матеріали з базового класу, або він не знайдений. Існує три загальних способи стати A
залежними:
using Bar<T>::A;
в класі - A
тепер посилається на щось в Bar<T>
, отже, залежне.
Bar<T>::A *x = 0;
в точці використання - Знову ж таки, A
безумовно, в Bar<T>
. Це множення, оскільки typename
не використовувалося, тому, можливо, поганий приклад, але нам доведеться почекати, поки operator*(Bar<T>::A, x)
миттєво з’ясуємо, чи повертається ревальвація. Хто знає, може, і так ...
this->A;
в точці використання - A
є членом, тому якщо він не входить, Foo
він повинен бути в базовому класі, знову ж таки стандарт каже, що це робить його залежним.
Двофазна компіляція є химерною і складною і вводить деякі дивовижні вимоги до додаткової багатослівності у вашому коді. Але скоріше, як демократія, це, мабуть, найгірший можливий спосіб робити, крім усіх інших.
Ви можете обґрунтовано стверджувати, що у вашому прикладі return x;
немає сенсу, якщо x
це вкладений тип у базовому класі, тому мова повинна (а) сказати, що це залежне ім'я та (2) трактувати його як нетип, і ваш код буде працювати без this->
. Наскільки ви стали жертвою побічної шкоди від вирішення проблеми, яка не застосовується у вашому випадку, але все ще існує проблема вашого базового класу, яка потенційно може вводити під вами тіні в глобальному масштабі імена, або не маєте імен, про які ви думали вони мали, а глобальну істоту знайшли замість цього.
Ви також можете стверджувати, що за замовчуванням має бути протилежне залежним іменам (припустимо, тип, якщо якимось чином не вказано, що це об'єкт), або що типовий варіант повинен бути більш контекстним (в std::string s = "";
, std::string
можна читати як тип, оскільки нічого іншого не робить граматичним сенс, хоч std::string *s = 0;
і неоднозначний). Знову ж таки, я не знаю зовсім, як правила були узгоджені. Я здогадуюсь, що кількість сторінок тексту, які були б потрібні, пом'якшували створення багатьох специфічних правил, для яких контексти приймають тип, а які - нетипові.