Problem dependant names

  • cpp, pl
  • finished

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;
}