Name hiding

Name hiding (ukrywanie nazw) to dość zaskakująca cecha z którą spotyka się prędzej czy później każdy programista C++.

Weźmy pod uwagę poniższy kod:

struct Base {
    void foo(std::string) { std::cout << "foo(string)" << std::endl; }
};

struct Derived : public Base {
    void foo(int) { std::cout << "foo(int)" << std::endl; }
};

int main() {
    Derived d;
    d.foo("abc");
}

Intuicyjnie kod powinien się skompilować i wypisać na standardowe wyjście nazwę wywołanej funkcji, w tym przypadku "foo(string)". Tak się jednak nie stanie, ponieważ zgodnie z zasadami języka deklaracja funkcji w scopie podrzędnym przykrywa (lub ukrywa, jak kto woli) deklarację w scopie nadrzędnym. Wobec tego powyższy program nie dość, że nie wypisze “poprawnej” funkcji (z punktu widzenia zdrowego na rozumie programisty), to na dodatek w ogóle się nie skompiluje, gdyż kompilator nie zna żadnej konwersji z inta na stringa!

Dlaczego tak się dzieje?

Zanim opiszę sposoby umożliwiające wywołanie funkcji foo z klasy bazowej, powiem o powodzie, dla którego została ona ukryta.

Niektórzy próbują w jakiś sposób racjonalizować tę cechę języka C++. Nie będę przytaczał w tym miejscu konkretnych przykładów (są dostępne np. tu i tu), gdyż według mnie są całkowicie niepoprawne.

W moim odczuciu uzasadnieniem przykrywania metod jest fakt, że nie jest to cecha specyficzna jedynie dla klas po sobie dziedziczących, ale generalna dla normalnego przeszukiwania nazw (unqualified name lookup). Przypomnijmy sobie z grubsza jak się ono odbywa:

Wynika to z faktu, że wywołując funkcję wewnątrz danego zakresu zazwyczaj mamy na myśli użycie funkcji zadeklarowanej w tymże zakresie. Na przykład jeśli zadeklarujemy funkcję bar::find, to wewnątrz zakresu bar raczej będziemy chcieli używać jej, niż jakiejkolwiek innej (np. z używanej przez nas, napisanej w C biblioteki libHardcoreFindAlgorithms). Co więcej, dzięki przykryciu funkcji find z zakresów nadrzędnych, funkcja bar::find może mieć dokładnie taką samą sygnaturę, czyli być czymś w rodzaju specjalizacji.

Mam nadzieję, że następujący przykład doskonale to zilustruje:

namespace bar {
    void foo(char) { }

    namespace baz {
        void foo(int) { }
        void call() { foo('a'); }
    }
}

int main() {
    // call() wywoła foo(int), chociaż foo(char) mogłoby być
    // najlepszym dopasowaniem
    bar::baz::call()
}

Jak uwzględnić deklaracje ze scope’ów nadrzędnych?

Wróćmy do naszego pierwszego przykładu. Aby wywołać funkcję Base::foo(string) możemy użyć następującej składni:

int main() {
    Derived d;
    d.Base::foo("abc");
}

Zanim jednak rzucimy się do edytorów tekstu, odpowiedzmy sobie na jedno bardzo ważne pytanie: kiedy ostatnio widzieliśmy taką składnię? Poza ezoterycznymi przykładami znalezionymi na wątpliwych blogach internetowych, nie sądzę aby cieszyła się ona większą popularnością. Z tego powodu powinniśmy użyć innej składni - dyrektywy using. Pisząc using Base::foo; udostępniamy wszystkie przeładowania funkcji Base::foo jako możliwe do wzięcia pod uwagę przy przeszukiwaniu nazw w bieżącym zakresie. Zatem modyfikując klasę Derived otrzymamy co następuje:

struct Derived : public Base {
    using Base::foo;
    void foo(int) { std::cout << "foo(int)" << std::endl; }
};

  1. Oczywiście name lookup jest nieco bardziej skomplikowany; uwzględnia na przykład fakt istnienia funkcji szablonowych (czyt.: nie są one powodem przerwania przeszukiwania). Gdy w wyniku normalnego przeszukiwania nie zostanie odnaleziona odpowiednia funkcja, do gry wchodzi ADL, ale to już oddzielny temat…