Коротка відповідь: щоб зробити 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;і неоднозначний). Знову ж таки, я не знаю зовсім, як правила були узгоджені. Я здогадуюсь, що кількість сторінок тексту, які були б потрібні, пом'якшували створення багатьох специфічних правил, для яких контексти приймають тип, а які - нетипові.