Krótka odpowiedź: aby utworzyć x
zależną nazwę, aby wyszukiwanie zostało odroczone do momentu poznania parametru szablonu.
Długa odpowiedź: gdy kompilator zobaczy szablon, powinien natychmiast wykonać pewne kontrole, nie widząc parametru szablonu. Inne są odraczane do momentu poznania parametru. Nazywa się to kompilacją dwufazową, a MSVC tego nie robi, ale jest wymagana przez standard i implementowana przez inne główne kompilatory. Jeśli chcesz, kompilator musi skompilować szablon, gdy tylko go zobaczy (do pewnego rodzaju wewnętrznej reprezentacji drzewa parsowania) i odłożyć kompilację na później.
Kontrole przeprowadzane na samym szablonie, a nie na poszczególnych jego instancjach, wymagają, aby kompilator mógł rozwiązać gramatykę kodu w szablonie.
W C ++ (i C), aby rozwiązać gramatykę kodu, czasami trzeba wiedzieć, czy coś jest typem, czy nie. Na przykład:
#if WANT_POINTER
typedef int A;
#else
int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }
jeśli A jest typem, który deklaruje wskaźnik (bez efektu innego niż cień globalny x
). Jeśli A jest obiektem, jest to zwielokrotnianie (a blokowanie przeciążania przez operatora jest nielegalne, przypisywanie do wartości). Jeśli jest niepoprawny, błąd ten musi zostać zdiagnozowany w fazie 1 , zgodnie ze standardem jest zdefiniowany jako błąd w szablonie , a nie w określonej instancji. Nawet jeśli szablon nigdy nie jest tworzony, jeśli A jest int
a, to powyższy kod jest źle sformułowany i musi zostać zdiagnozowany, tak jak by to było, gdyby w foo
ogóle nie był szablonem, ale zwykłą funkcją.
Teraz standard mówi, że nazwy, które nie są zależne od parametrów szablonu, muszą być rozpoznawalne w fazie 1. A
tutaj nie jest nazwa zależna, odnosi się do tej samej rzeczy bez względu na typ T
. Dlatego należy go zdefiniować przed zdefiniowaniem szablonu, aby można go znaleźć i sprawdzić w fazie 1.
T::A
byłoby nazwą zależną od T. W fazie 1 prawdopodobnie nie wiemy, czy jest to typ, czy nie. Typ, który ostatecznie zostanie użyty jako T
instancja, najprawdopodobniej nie jest jeszcze zdefiniowany, a nawet jeśli tak, nie wiemy, który typ zostanie użyty jako parametr szablonu. Ale musimy rozwiązać gramatykę, aby wykonać nasze cenne testy w fazie 1 pod kątem źle sformułowanych szablonów. Zatem standard ma regułę dla nazw zależnych - kompilator musi założyć, że nie są typami, chyba że kwalifikuje się typename
do określenia, że są typami lub są używane w pewnych jednoznacznych kontekstach. Na przykład, w template <typename T> struct Foo : T::A {};
, T::A
stosuje się jako bazową, a więc jednoznacznie typu. Jeśli Foo
jest tworzone z jakiegoś typu, który ma członka danychA
zamiast zagnieżdżonego typu A jest to błąd w kodzie tworzącym instancję (faza 2), a nie błąd w szablonie (faza 1).
Ale co z szablonem klasy z zależną klasą bazową?
template <typename T>
struct Foo : Bar<T> {
Foo() { A *x = 0; }
};
Czy nazwa zależna, czy nie? W przypadku klas podstawowych każda nazwa może pojawić się w klasie podstawowej. Moglibyśmy więc powiedzieć, że A jest nazwą zależną i traktować ją jako nietypową. Miałoby to niepożądany efekt, że każda nazwa w Foo jest zależna, a zatem każdy typ użyty w Foo (oprócz typów wbudowanych) musi zostać zakwalifikowany. Wewnątrz Foo musisz napisać:
typename std::string s = "hello, world";
ponieważ std::string
byłaby to nazwa zależna, a zatem zakłada się, że nie jest typem, chyba że określono inaczej. Auć!
Drugi problem z zezwoleniem na preferowany kod ( return x;
) polega na tym, że nawet jeśli Bar
został wcześniej zdefiniowany Foo
i x
nie jest członkiem tej definicji, ktoś może później zdefiniować specjalizację Bar
dla jakiegoś typu Baz
, na przykład taką, Bar<Baz>
która ma element danych x
, a następnie utworzyć instancję Foo<Baz>
. Tak więc w tej instancji szablon zwróciłby element danych zamiast zwracać wartość globalną x
. Lub odwrotnie, jeśli podstawowa definicja szablonu Bar
miałaby x
, mogliby zdefiniować specjalizację bez niej, a Twój szablon szukałby globalnej wartości, x
do której mógłby wrócić Foo<Baz>
. Myślę, że uznano to za równie zaskakujące i niepokojące jak problem, który masz, ale to po cichu zaskakujące, w przeciwieństwie do rzucania zaskakującego błędu.
Aby uniknąć tych problemów, obowiązujący standard mówi, że zależne klasy podstawowe szablonów klas po prostu nie są brane pod uwagę przy wyszukiwaniu, chyba że jest to wyraźnie wymagane. To powstrzymuje wszystko od uzależnienia tylko dlatego, że można je znaleźć w zależnej bazie. Ma również niepożądany efekt, który widzisz - musisz zakwalifikować rzeczy z klasy podstawowej, w przeciwnym razie nie zostanie znaleziony. Istnieją trzy popularne sposoby A
uzależnienia:
using Bar<T>::A;
w klasie - A
teraz odnosi się do czegoś Bar<T>
, co jest zależne.
Bar<T>::A *x = 0;
w miejscu użycia - Znowu, A
zdecydowanie jest w Bar<T>
. Jest to mnożenie, ponieważ typename
nie zostało użyte, więc prawdopodobnie zły przykład, ale będziemy musieli poczekać do wystąpienia, aby dowiedzieć się, czy operator*(Bar<T>::A, x)
zwraca wartość. Kto wie, może to robi ...
this->A;
w miejscu użycia - A
jest członkiem, więc jeśli go nie ma Foo
, musi należeć do klasy podstawowej, ponownie standard mówi, że to uzależnia.
Kompilacja dwufazowa jest skomplikowana i trudna, i wprowadza pewne zaskakujące wymagania dotyczące dodatkowego języka w kodzie. Ale raczej, jak demokracja, jest to prawdopodobnie najgorszy możliwy sposób, oprócz wszystkich innych.
Można zasadnie argumentować, że w twoim przykładzie return x;
nie ma sensu, jeśli x
jest to typ zagnieżdżony w klasie bazowej, więc język powinien (a) powiedzieć, że jest to nazwa zależna i (2) traktować go jako nietypowy, i twój kod działałby bez this->
. W pewnym stopniu jesteś ofiarą szkód ubocznych spowodowanych rozwiązaniem problemu, który nie ma zastosowania w twoim przypadku, ale wciąż istnieje problem, że twoja klasa podstawowa potencjalnie wprowadza pod tobą nazwy, które cieniują globale, lub nie ma imion, o których myślałeś mieli, a zamiast tego znaleziono globalną istotę.
Można również argumentować, że wartość domyślna powinna być odwrotna dla nazw zależnych (zakładając, że typ nie jest określony jako obiekt), lub że wartość domyślna powinna być bardziej wrażliwa na kontekst (w std::string s = "";
, std::string
może być odczytana jako typ, ponieważ nic innego nie czyni gramatyki sens, choć std::string *s = 0;
jest niejednoznaczny). Ponownie nie wiem dokładnie, w jaki sposób uzgodniono zasady. Domyślam się, że liczba stron tekstu, które byłyby wymagane, złagodziła potrzebę stworzenia wielu szczegółowych reguł, dla których konteksty przyjmują typ, a które nie są typem.