Dlaczego muszę uzyskiwać dostęp do elementów klasy podstawowej szablonu za pomocą tego wskaźnika?

199

Gdyby poniższe klasy nie były szablonami, po prostu mógłbym je mieć xw derivedklasie. Jednak z poniższym kodzie I trzeba użytku this->x. Czemu?

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;
}
Ali
źródło
1
Ach, Jezu. Ma to coś wspólnego z wyszukiwaniem nazw. Jeśli ktoś wkrótce nie odpowie na to pytanie, sprawdzę go i opublikuję (teraz zajęty).
templatetypedef
@Ed Swangren: Przepraszam, nie znalazłem go wśród oferowanych odpowiedzi, publikując to pytanie. Już dawno szukałem odpowiedzi.
Ali
6
Dzieje się tak z powodu dwufazowego wyszukiwania nazw (które nie są domyślnie używane przez wszystkie kompilatory) i nazw zależnych. Są 3 rozwiązania tego problemu, inne niż dodanie do xz this->, a mianowicie: 1) używać prefiksu base<T>::x, 2) dodać oświadczenie using base<T>::x, 3) użyj globalnego przełącznik kompilatora, który umożliwia tryb dopuszczający. Zalety i wady tych rozwiązań opisano w stackoverflow.com/questions/50321788/...
George Robinson

Odpowiedzi:

274

Krótka odpowiedź: aby utworzyć xzależ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 inta, to powyższy kod jest źle sformułowany i musi zostać zdiagnozowany, tak jak by to było, gdyby w fooogó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. Atutaj 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::Abył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 Tinstancja, 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ę typenamedo określenia, że typami lub są używane w pewnych jednoznacznych kontekstach. Na przykład, w template <typename T> struct Foo : T::A {};, T::Astosuje się jako bazową, a więc jednoznacznie typu. Jeśli Foojest 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::stringbył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 Barzostał wcześniej zdefiniowany Fooi xnie jest członkiem tej definicji, ktoś może później zdefiniować specjalizację Bardla 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 Barmiałaby x, mogliby zdefiniować specjalizację bez niej, a Twój szablon szukałby globalnej wartości, xdo 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 Auzależnienia:

  • using Bar<T>::A;w klasie - Ateraz odnosi się do czegoś Bar<T>, co jest zależne.
  • Bar<T>::A *x = 0;w miejscu użycia - Znowu, Azdecydowanie jest w Bar<T>. Jest to mnożenie, ponieważ typenamenie 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 - Ajest 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 xjest 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::stringmoż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.

Steve Jessop
źródło
1
Och, ładna szczegółowa odpowiedź. Wyjaśniłem kilka rzeczy, których nigdy nie zadałem sobie trudu, by spojrzeć w górę. :) +1
jalf
20
@jalf: czy istnieje coś takiego jak C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - „Pytania, które będą często zadawane, z wyjątkiem tego, że nie jesteś pewien, czy chcesz znać odpowiedź i mieć ważniejsze rzeczy do zrobienia”?
Steve Jessop,
4
niezwykła odpowiedź, zastanawiam się, czy pytanie pasuje do pytania.
Matthieu M.,
Whoa, możemy powiedzieć encyklopedycznie? highfive Jedna subtelna kwestia: „Jeśli instancja Foo jest utworzona z jakiegoś typu, który ma element danych A zamiast typu zagnieżdżonego A, jest to błąd w kodzie wykonującym tworzenie instancji (faza 2), a nie błąd w szablonie (faza 1). ” Może lepiej powiedzieć, że szablon nie jest zniekształcony, ale nadal może to być przypadek niepoprawnego założenia lub błędu logicznego ze strony autora szablonu. Jeśli oznaczona instancja była rzeczywiście zamierzonym przypadkiem użycia, szablon byłby nieprawidłowy.
Ionoclast Brigham
1
@JohnH. Biorąc pod uwagę, że kilka kompilatorów implementuje -fpermissivelub podobne, tak, jest to możliwe. Nie znam szczegółów jego implementacji, ale kompilator musi xodkładać rozpoznawanie, dopóki nie pozna rzeczywistej podstawowej klasy tymczasowej T. Tak więc, w zasadzie w trybie niedozwolonym, może on rejestrować fakt, że go odroczył, odracza go, wykona wyszukiwanie, gdy już to zrobi T, a gdy wyszukiwanie zakończy się powodzeniem, wyda sugerowany tekst. Byłaby to bardzo trafna sugestia, gdyby została wydana tylko w przypadkach, w których działa: szanse, że użytkownik miał na myśli inny xz jeszcze innego zakresu, są bardzo małe!
Steve Jessop
13

(Oryginalna odpowiedź z 10 stycznia 2011 r.)

Myślę, że znalazłem odpowiedź: problem GCC: użycie członka klasy podstawowej, która zależy od argumentu szablonu . Odpowiedź nie jest specyficzna dla gcc.


Aktualizacja: W odpowiedzi na komentarz mmichaela z projektu N3337 standardu C ++ 11:

14.6.2 Nazwy zależne [temp.dep]
[...]
3 W definicji klasy lub szablonu klasy, jeśli klasa podstawowa zależy od parametru szablonu, zakres klasy podstawowej nie jest badany podczas wyszukiwania nazwy bez zastrzeżeń w punkt definicji szablonu klasy lub elementu lub podczas tworzenia szablonu klasy lub elementu.

Czy „ponieważ standard tak mówi” liczy się jako odpowiedź, nie wiem. Możemy teraz zapytać, dlaczego standard tak nakazuje, ale jak podkreślają doskonała odpowiedź Steve'a Jessopa i inni, odpowiedź na to ostatnie pytanie jest dość długa i dyskusyjna. Niestety, jeśli chodzi o standard C ++, często jest prawie niemożliwe, aby udzielić krótkiego i niezależnego wyjaśnienia, dlaczego standard coś nakazuje; dotyczy to również tego ostatniego pytania.

Ali
źródło
11

xJest ukryty podczas spadku. Możesz odkryć za pomocą:

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

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};
chrisaycock
źródło
25
Ta odpowiedź nie wyjaśnia, dlaczego jest ukryta.
jamesdlin 10.01.11