Почему мне приходится обращаться к членам базового класса шаблонов через этот указатель?

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

Краткий ответ: чтобы сделать 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 с элементом данных 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 можно читать как type, так как ничто иное не имеет грамматического смысла, хотя std::string *s = 0; является неоднозначным). Опять же, я не знаю, как были согласованы правила. Я предполагаю, что количество страниц текста, которое может потребоваться, сведено к минимуму с созданием множества конкретных правил, для которых контексты принимают тип, а какой - нетип.

Ответ 2

(исходный ответ от 10 января 2011 г.)

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


Обновление: В ответ на комментарий mmichael, из проект N3337 стандарта С++ 11:

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

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

Ответ 3

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

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

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