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