Problem dependant names
C++ jest językiem trudnym nie tylko dla ludzi - kompilatory i ich twórcy też nie mają lekkiego życia. Problemów przysparza już samo określenie czy jego gramatyka jest kontekstowa, czy nie. Zaś źródłem największych problemów niemal wszystkich programistów C++ jest nic innego jak szablony.
Zazwyczaj widząc pewien fragment kodu w C++ od razu jesteśmy w stanie go logicznie (i poprawnie) zinterpretować. Wynika to z tego, że jako oczytani programiści, znamy dobrze kontekst danego fragmentu, stosowane techniki i wzorce projektowe itp. Jednak kompilator tej wiedzy nie ma - jemu musi wystarczyć nasz (poprawny bądź nie) kod źródłowy oraz standard języka.
W szczególności problemów przysparza interpretacja składowych zależnych
szablonu. Mowa oczywiście o tytułowym dependant name, czyli po polsku
nazwie zależnej. Nazywamy tak każdy identyfikator zależny od parametru
szablonowego (w formie np. T::name
). Jego prawdziwe oblicze nie jest znane aż
do momentu jego instantacji, a zatem w momencie jego parsowania.
Słowo typename
Weźmy jako przykład następujący fragment kodu:
int s = 3;
template <typename T>
void foo()
{
T::iterator * s;
}
W powyższym iterator
jest nazwą zależną, ponieważ zależy od typu szablonowego
T
. I teraz pytanie
- czy ciało funkcji foo()
deklaruje lokalnie zmienną wskaźnikową s
, czy też
może mnoży zmienną statyczną będącą składową jakiegoś typu T przez wartość
zmiennej globalnej s
? Innymi słowy: które z poniższych jest poprawne?
struct A { static int iterator; };
struct B { using iterator = int; };
foo<A>();
foo<B>();
Standard [p. 14.6-2] jasno określa, że w takich przypadkach powyższy fragment należy sparsować, być może ku zdziwieniu większości, jako nie-typ. Programista, jeśli chce aby dany fragment był zinterpretowany jako typ (jako deklaracja zmiennej), musi użyć słowa kluczowego typename:
template <typename T>
void foo()
{
typename T::iterator * s;
}
Na zakończenie akapitu podam być może bardziej rzeczywisty przypadek:
template <typename Container>
bool defaultInside(const Container& c)
{
typename Container::iterator start = c.begin();
typename Container::iterator end = c.end();
typename Container::value_type def = Container::value_type();
return std::find(start, end, def) == end;
}
Użycie słowa kluczowego typename, z powodu właśnie iteratorów, jest
prawdopodobnie najbardziej znanym rozróżnieniem dwóch możliwych interpretacji
kodu (chociaż w erze auto
prawdopodobnie traci nieco na wartości).
Słowo template
Innym źródłem niejednoznaczności jest sam fakt doboru znaków ograniczających
listę parametrów szablonowych: < >
. Zupełnie poprawną interpretacją tychże
jest przecież uznanie ich za matematyczne znaki mniejszości i większości. I
takie sparsowanie nakazuje właśnie standard [p. 14.2-4], chyba że zostanie
użyte słowo kluczowe template.
template <typename T>
struct Foo
{
template <typename U> void foo(){}
};
template<typename T>
void bar()
{
Foo<T> f;
f.foo<T>(); // błąd kompilacji
// interpretacja jako f.foo mniejsze od jakiegoś T
f.template foo<T>(); // ok
}
Takie użycie słowa template
jest dozwolone po operatorach ::
(zakresu), .
(dostępu do membera klasy) i ->
(dostępu do membera klasy poprzez wskaźnik):
T::template foo<X>();
t.template foo<X>();
t->template foo<X>();
Sytuację nieco upraszcza/komplikuje (niepotrzebne skreślić) fakt, że standard
[p. 14.2-3] definiuje procedurę parsowanie w sytuacji gdy name lookup
stwierdzi, że dana nazwa jest w istocie nazwą szablonu. W takim przypadku znak
<
musi zostać uznany jako początek listy typów szablonu, a nie znak
mniejszości, zaś użycie słowa template
w powyżej opisany sposób nie jest
niezbędne. Dzięki temu nie jesteśmy np. zmuszeni pisać std::template
function<int()> f;
(mimo że jest to całkowicie poprawny zapis).
Słowem zakończenia dodam, że słowa typename
i template
można dodawać
nadmiarowo, czyli nawet wówczas gdy nie są wymagane, gdyż parser języka jest
sobie w stanie bez nich poradzić:
template<typename T>
struct Foo {
template<typename U> void foo(){}
using iterator = int;
};
// Całkiem poprawna funkcja
template<typename T>
void bar()
{
Foo<int> f; // podmiana Foo<T> -> Foo<int> powoduje,
// że nie jest to już dependant name. Łatwizna.
f.foo<T>();
f.template foo<T>();
Foo<int>::iterator it1;
typename Foo<int>::iterator it2;
}