Почему я должен получить доступ к членам базового класса шаблона через указатель this? - PullRequest
175 голосов
/ 10 января 2011

Если бы приведенные ниже классы не были шаблонами, я мог бы просто иметь 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;
}

Ответы [ 3 ]

247 голосов
/ 10 января 2011

Краткий ответ: чтобы сделать 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 - это объект, это умножение (и запрет на перегрузку некоторых операторов недопустимо, присваивая значение r).Если это не так, эта ошибка должна быть диагностирована в фазе 1 , согласно стандарту она определяется как ошибка в шаблоне , а не в какой-то конкретной ее реализации.Даже если шаблон никогда не создается, если A - int, то приведенный выше код некорректен и должен быть диагностирован, как если бы foo был не шаблоном вообще, а простой функцией.

Теперь в стандарте говорится, что имена, которые не не зависят от параметров шаблона, должны быть разрешены на этапе 1. A здесь это не зависимое имя, оно относится к одному и тому женезависимо от типа T.Поэтому его необходимо определить до того, как шаблон будет определен, чтобы его можно было найти и проверить на этапе 1.

T::A - это имя, которое зависит от T. На этапе 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) значение r.Кто знает, может быть, это так ...
  • this->A; в точке использования - A является членом, поэтому, если он не входит в Foo, он должен быть в базовом классе, опять же в стандартеговорит, что это делает его зависимым.

Двухфазная компиляция сложна и сложна, и вводит некоторые неожиданные требования для дополнительной словесности в вашем коде.Но, скорее, как и демократия, это, пожалуй, худший из возможных способов ведения дел, помимо всех остальных.

Вы можете обоснованно утверждать, что в вашем примере return x; не имеет смысла, если x является вложеннымвведите базовый класс, поэтому язык должен (а) сказать, что это зависимое имя и (2) трактовать его как нетип, и ваш код будет работать без this->.В какой-то степени вы стали жертвой сопутствующего ущерба от решения проблемы, которая не применима в вашем случае, но все еще существует проблема с вашим базовым классом, потенциально представляющим имена под вами, которые являются теневыми глобалами, или отсутствие имен, о которых вы думалиони имели, и вместо этого был найден глобальный.

Можно также утверждать, что значение по умолчанию должно быть противоположным для зависимых имен (предполагается, что тип, если как-то не определено как объект), или что значение по умолчанию должно быть большеконтекстно-зависимый (в std::string s = "";, std::string может быть прочитан как тип, поскольку ничто иное не имеет грамматического смысла, даже если std::string *s = 0; неоднозначно).Опять же, я не знаю, как были согласованы правила.Я предполагаю, что количество страниц текста, которое может потребоваться, сведено к минимуму с созданием множества конкретных правил, для которых контексты принимают тип, а какой - нетип.

12 голосов
/ 10 января 2011

(Оригинальный ответ от 10 января 2011 г.)

Мне кажется, я нашел ответ: Проблема GCC: использование члена базыкласс, который зависит от аргумента шаблона .Ответ не является специфичным для gcc.


Обновление: В ответ на комментарий Микаэля из черновика N3337 стандарта C ++ 11:

14.6.2 Зависимые имена [temp.dep]
[...]
3 Если в определении класса или шаблона класса базовый класс зависит отпараметр-шаблон, область действия базового класса не проверяется при поиске неквалифицированного имени ни в точке определения шаблона класса, ни в элементе, ни во время создания шаблона или элемента класса.

Является ли "потому что стандарт так говорит" считается ответом, я не знаю.Теперь мы можем спросить, почему стандарт предписывает это, но, как замечает Стив Джессоп и другие, ответ на этот последний вопрос довольно длинный и спорный.К сожалению, когда речь идет о стандарте C ++, зачастую почти невозможно дать краткое и самостоятельное объяснение того, почему стандарт требует чего-либо;это относится и к последнему вопросу.

11 голосов
/ 10 января 2011

x скрыто во время наследования.Вы можете показать через:

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

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