Argument Dependent Lookup

  • cpp, pl
  • finished

Z racji tego, że czas na cokolwiek ostatnio mam jedynie wtedy gdy mój fork jest w trakcie wywoływania funkcji sleep, a wszystkie jego syscalle zdają się przechodzić przeze mnie, a nie przez kernel (dziwne, bo nie przypominam sobie, żebym w celach debugowych wywoływał na nim ptrace), muszę dość mocno ograniczyć zakres i objętość artykułów. Dlatego tym razem postaram się w paru żółnierskich słowach wytłumaczyć czym w C++ jest Argument Dependent Lookup. Nie chcę przy tym powiedzieć, że temat ADL jest prosty (nie jest) i łatwy do zrozumienia i zapamiętania (również nie jest), ale samą ideę można według mnie wytłumaczyć w dość prosty sposób.

Do rzeczy

ADL jest dodatkowym sposobem poszukiwania przez kompilator wywoływanych nazw funkcji w przypadku gdy zwyczajowe przeszukiwanie zawiedzie (pokrótce je opisałem przy okazji tematu ukrywania nazw). ADL, jak nazwa sugeruje, analizuje argumenty funkcji, a konkretnie ich namespace’y, i przeszukuje je pod kątem zgodności wywołania i deklaracji funkcji.

Innymi słowy, świadomie możemy pominąć namespace’y danej funkcji przy jej wywołaniu, pod warunkiem, że jej argumenty leżą w tej samej przestrzeni nazw co ona sama.

Przykładowo:

namespace foo
{

class A {};
int add(int a, int b, A&) { return a + b; }

namespace bar
{

class B {};
int prod(int a, int b, A&) { return a * b; }
int prod(int a, int b, B&) { return a * b; }

}  // namespace bar
}  // namespace foo

int main()
{
    foo::A a;
    foo::bar::B b;

    auto i = add(1, 2, a);
    //auto j = add(3, 4, b);  // error
    auto k = prod(7, 8, b);
    // auto l = prod(5, 6, a);  // error
}

Jak widać, jedynie bezpośrednie namespace’y argumentów są dodawane do przeszukiwania. Oczywiście, nie poczuwam się do odgrywania roli podręcznika C++, a reguł jest znacznie więcej (np. w przypadku gdy argumentem jest wskaźnik na funkcję, to przeszukiwane są również wartości zwracane wraz ze zbiorem powiązanych z nimi klas i namespace’ów). Po szczegóły odsyłam do faktycznego podręcznika: cppreference.com.

swap

Idiom copy-and-swap na stałe zagościł w narzędziowniku programistów C++. W telegraficznym skrócie: dla klas, dla których piszemy konstruktor kopiujący powinniśmy również utworzyć publiczną wolną funkcję swap. Wynika to np. z tego, że cała biblioteka standardowa wykorzystuje ADL dla wywołań funkcji swap. Przebiega to następująco:

template <typename Iter1, typename Iter2>
void iter_swap(Iter1 lhs, Iter2 rhs)
{
    using std::swap;
    swap(*lhs, *rhs);
}

Spowoduje to wywołanie specjalizowanej wersji swapa (przeszukane zostaną namespace’y odpowiednie dla *lhs i *rhs), a w przypadku gdyby owa się nie znalazła, dzięki użyciu using std::swap istnieje możliwość cichego odwrotu w stronę domyślnej implementacji z biblioteki standardowej.

Interfejsy

Funkcje odnajdywane przez ADL są uznawane za część interfejsu klas, które są ich parametrami. Dlaczego ma to sens? Wyobraźmy sobie, że chcielibyśmy np. wypisać tekstową reprezentację jakiejś klasy. Jak to robimy? Na przykład przeładowujemy operator<<:

namespace foo
{

struct Foo
{
    std::string val;
}

std::ostream& operator<<(std::ostream& s, const Foo& foo)
{
    os << "Foo(val=" << foo.val << ")";
    return os;
}

}  // namespace foo

int main()
{
    Foo foo{"abc"};
    std::cout << "my foo: " << foo << std::endl;
}

W powyższym operator<< zawiera tak naprawdę newralgiczny fragment interfejsu Foo, czyli prezentację owej struktury użytkownikowi. Funkcja ta jest natomiast odnajdywana tylko i wyłącznie dzięki ADL, gdyż cout << jest tak naprawdę niekwalifikowanym wywołaniem operator<<.

Innymi słowy:

std::cout << "my foo: " << foo << std::endl;

jest równoważne

operator<<(std::cout, "my foo: ");
operator<<(std::cout, foo);
endl(std::cout);

Warto o tym pamiętać, gdyż każda zmiana funkcji odnajdywanych przez ADL jest de facto zmianą interfejsu klasy i może być niekompatybilna wstecznie, albo spowodować błędne lub niespodziewane działanie programu. Na przykład usunięcie specjalizowanej implementacji swap nie zepsuje kompilacji programu, a raczej spowoduje wycofanie się do std::swap, który będzie zamieniał przekazane wartości w inny sposób niż odbywało się to wcześniej.