Чому я маю доступ до членів базового класу шаблонів через цей покажчик?


199

Якщо нижче класи були шаблони я міг би просто мати xв derivedкласі. Однак із наведеним нижче кодом мені доводиться користуватися this->x. Чому?

template <typename T>
class base {

protected:
    int x;
};

template <typename T>
class derived : public base<T> {

public:
    int f() { return this->x; }
};

int main() {
    derived<int> d;
    d.f();
    return 0;
}

1
Ах, джез. Це має щось спільне з пошуком імен. Якщо хтось не відповість на це незабаром, я перегляну його та опублікую (зараз зайнятий).
templatetypedef

@Ed Swangren: Вибачте, я пропустив його серед запропонованих відповідей під час публікації цього питання. Я довго шукав відповідь до цього.
Алі

6
Це відбувається через пошук двофазних імен (який використовують не всі компілятори за замовчуванням) та залежні імена. Існує 3 рішення цієї проблеми, окрім префіксу xдо this->, а саме: 1) Використання префікса base<T>::x, 2) Додати висловлювання using base<T>::x, 3) Використовувати глобальний перемикач компілятора, який дозволяє режим дозволу. Плюси і мінуси цих рішень описані в stackoverflow.com/questions/50321788/…
Джордж Робінсон

Відповіді:


274

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


1
О, приємна детальна відповідь. Уточнив пару речей, які я ніколи не намагався підняти погляд. :) +1
jalf

20
@jalf: чи існує таке поняття, як C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - "Питання, які часто задаватимуться, за винятком того, що ви не впевнені, що навіть хочете дізнатися відповідь і маєте важливіші речі, з якими потрібно працювати"?
Стів Джессоп

4
неординарна відповідь, цікавіться, чи питання може вміститися у файлі.
Матьє М.

Хто може, ми можемо сказати, енциклопедичний? highfive Один найтонший пункт, однак: "Якщо Foo примірник якогось типу, який має член даних A замість вкладеного типу A, це помилка в коді, що робить інстанціювання (фаза 2), а не помилка в шаблоні (фаза 1). " Можливо, краще сказати, що шаблон не є деформованим, але це все-таки може бути випадком неправильного припущення або помилки логіки з боку розробника шаблону. Якщо позначена інстанція насправді була призначеною справою використання, то шаблон буде неправильним.
Ionoclast Brigham

1
@JohnH. З огляду на те, що кілька компіляторів реалізують -fpermissiveабо подібні, так, це можливо. Я не знаю деталей того, як це реалізовано, але компілятор повинен відкладати розв’язання, xпоки не дізнається фактичний базовий клас шаблону T. Отже, в принципі в недозволеному режимі він міг записати той факт, який він відклав, відкладіть, зробіть пошук, коли він має бути T, і коли пошук стане успішним, видайте запропонований вами текст. Це було б дуже точною пропозицією, якби це було зроблено лише у тих випадках, коли це працює: шанси, що користувач мав на увазі якийсь інший xіз ще однієї сфери, досить малі!
Стів Джессоп

13

(Оригінальна відповідь від 10 січня 2011 р.)

Я думаю, що я знайшов відповідь: Проблема GCC: використання члена базового класу, який залежить від аргументу шаблону . Відповідь не характерна для gcc.


Оновлення: У відповідь на коментар mmichael , з проекту N3337 стандарту C ++ 11:

14.6.2 Залежні імена [temp.dep]
[...]
3 У визначенні класу або шаблону класу, якщо базовий клас залежить від параметра-шаблону, область базового класу не досліджується під час некваліфікованого пошуку імені або при точка визначення шаблону або члена класу або під час інстанціювання шаблону класу або члена.

Чи вважається "тому, що стандарт говорить так", як відповідь, я не знаю. Тепер ми можемо запитати, чому стандартний мандат це, але, як вказують на відмінну відповідь Стіва Джессопа та інші, відповідь на це останнє питання досить довгий і спірний. На жаль, якщо мова йде про стандарт C ++, то часто майже неможливо дати коротке і самодостатнє пояснення того, чому стандарт щось вимагає; це стосується і останнього питання.


11

xПрихований під час успадкування. Ви можете скасувати показ за допомогою:

template <typename T>
class derived : public base<T> {

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};

25
Ця відповідь не пояснює, чому це приховано.
jamesdlin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.