Close Menu
    Ciekawe

    Kodeki audio i wideo – paczki niezbędne do odtwarzania filmów

    2026-05-18

    Programy antyplagiatowe – jak sprawdzić unikalność tekstu?

    2026-05-17

    Jak przenieść pliki z tabletu na pendrive? Podłączanie i transfer danych

    2026-05-16
    Facebook X (Twitter) Instagram
    CPP Polska
    Facebook X (Twitter) Instagram
    • Biznes

      Jak sprawdzić pomysł na biznes? MVP a badania konsumenckie

      2026-05-13

      Jak karty lojalnościowe wspierają sprzedaż i budują lojalność klientów?

      2026-05-11

      Karta paliwowa dla małej firmy – jaką wybrać i czy to się opłaca?

      2026-04-21

      Jak wymyślić i zastrzec nazwę firmy? Poradnik i sprawdzanie dostępności

      2026-04-08

      Programy VPN – ranking, porównanie i poradnik wyboru (2026)

      2026-02-26
    • Technologie

      Kodeki audio i wideo – paczki niezbędne do odtwarzania filmów

      2026-05-18

      Programy antyplagiatowe – jak sprawdzić unikalność tekstu?

      2026-05-17

      Jak przenieść pliki z tabletu na pendrive? Podłączanie i transfer danych

      2026-05-16

      CDex – zgrywanie muzyki z płyt CD Audio do formatu MP3

      2026-05-15

      Speccy – jak sprawdzić specyfikację i temperaturę podzespołów?

      2026-05-14
    • Programowanie

      Maszyna stanów oparta o std::variant

      2025-10-07

      std::deque w C++ – kiedy wybrać dwukierunkową kolejkę zamiast vectora

      2025-10-07

      Tablice w C++ od podstaw – deklaracja, inicjalizacja, iteracja i typowe pułapki

      2025-10-07

      itoa i std::to_chars – konwersja liczb na tekst bez narzutu wydajności

      2025-10-07

      strcpy vs strncpy vs std::string – bezpieczne kopiowanie łańcuchów w C++

      2025-10-07
    • Inne

      Bezpieczeństwo finansowe w sektorze IT

      2026-04-29

      Tłumaczenia symultaniczne – klucz do sprawnej komunikacji na międzynarodowych wydarzeniach

      2026-03-26

      eSIM w Mobile Vikings – jak wirtualna karta SIM daje Ci wolność bez plastiku, kuriera i wychodzenia z domu

      2025-12-16

      Jak prowadzić blog programistyczny i dzielić się wiedzą?

      2025-06-28
    • Programy VPN – ranking
    CPP Polska
    Home»C++»ADL (Argument-dependent lookup) w praktyce
    C++

    ADL (Argument-dependent lookup) w praktyce

    Oskar KlimkiewiczBy Oskar KlimkiewiczBrak komentarzy11 Mins Read
    Share Facebook Twitter LinkedIn Email Copy Link
    Follow Us
    RSS
    person using macbook pro on white table
    Share
    Facebook Twitter LinkedIn Email Copy Link

    Argument-Dependent Lookup (ADL), znany także jako Koenig lookup, to fundamentalny mechanizm rozpoznawania nazw w języku C++, który w istotny sposób wpływa na sposób przetwarzania niekwalifikowanych wywołań funkcji podczas kompilacji. Ta zaawansowana reguła wyszukiwania umożliwia C++ przeszukiwanie deklaracji funkcji w przestrzeniach nazw powiązanych z typami argumentów, czyniąc składnię przeciążania operatorów oraz wzorców programowania generycznego bardziej naturalną. W przeciwieństwie do konwencjonalnego wyszukiwania nazw, które bada jedynie leksykalne otoczenie wywołującego, ADL rozszerza poszukiwania do przestrzeni nazw, w których zdefiniowane są typy argumentów, co tworzy system dwutorowego rozpoznawania i równoważy hermetyzację przestrzeni nazw z wygodą składni. Ten mechanizm okazuje się nieoceniony dla płynnej integracji typów niestandardowych z komponentami biblioteki standardowej, co szczególnie widać w operacjach strumieniowych, gdzie składnia std::cout << custom_object działa poprawnie mimo, że operator został zdefiniowany poza przestrzenią globalną. Poniższa analiza przybliża techniczne podstawy ADL, jego praktyczne zastosowania, niuanse implementacyjne i kontrowersje, ukazując jego kluczową rolę w skutecznym tworzeniu oprogramowania w C++ w różnych paradygmatach i dziedzinach zastosowań.

    Podstawowa mechanika argument-dependent lookup

    Podstawowy proces wyszukiwania

    Argument-dependent lookup działa jako dodatkowa faza rozpoznawania nazw, aktywna podczas rozwiązywania niekwalifikowanych wywołań funkcji. Gdy kompilator natrafia na wywołanie f(x,y), uruchamia dwutorowy proces wyszukiwania. Najpierw wykonuje standardowe wyszukiwanie niekwalifikowane, badając po kolei bieżący zakres oraz otaczające przestrzenie nazw. Jednocześnie ADL analizuje przestrzenie nazw powiązane z typami każdego z argumentów. Dla argumentu typu T przeszukuje przestrzeń, w której T został zadeklarowany, łącznie z przestrzeniami bazowymi, jeśli T jest typem pochodnym, a także rekurencyjnie rozważa przestrzenie nazw parametrów szablonowych, gdy T jest szablonem. Oznacza to, że funkcje widoczne przez otoczenie leksykalne lub przez powiązane przestrzenie nazw stają się kandydatami do rozwiązywania przeciążenia.

    Najważniejszym aspektem ADL są reguły powiązania przestrzeni nazw. Dla typów fundamentalnych, jak int czy double, nie istnieją powiązane przestrzenie, co ogranicza zakres ADL. Typy definiowane przez użytkownika wnoszą do rozważania całą przestrzeń deklaracji. Przy wielu argumentach, ADL analizuje przestrzenie związane z każdym typem argumentu, tworząc sumę możliwych przestrzeni. To włączające podejście zapewnia, że funkcje napisane z myślą o określonych typach mogą być odnalezione niezależnie od miejsca wywołania, co umożliwia bardziej naturalne wzorce projektowania API.

    Zasady wyłączenia przestrzeni nazw

    ADL wprowadza celowe ograniczenia w wybranych scenariuszach. Kiedy wywołanie funkcji jest jawnie kwalifikowane nazwą przestrzeni (np. std::swap(a,b)), ADL zostaje całkowicie pominięty. Zachowanie to pozwala programistom na precyzyjną kontrolę rozpoznawania nazw, ważną w sytuacjach, gdzie niezamierzone przeciążenia mogłyby wywołać niejednoznaczność. Ponadto, ADL nie jest aktywowany, jeśli standardowe wyszukiwanie niekwalifikowane znajdzie encję inną niż funkcja (zmienną, alias typu itp.) o odpowiadającej nazwie, nawet jeśli w powiązanej przestrzeni znajduje się lepsza funkcja. Zapobiega to zaskakującym przypadkom, w których lokalna deklaracja mogłaby przypadkowo przesłonić bardziej odpowiednią funkcję.

    Proces wyszukiwania jest szczególnie zaawansowany dla argumentów szablonowych. Dla typów szablonowych, jak ClassName<T1,T2>, ADL przeszukuje przestrzenie zarówno dla szablonu, jak i wszystkich parametrów typów szablonowych (łącznie z ich otaczającymi klasami i przestrzeniami). Nietypowe parametry szablonów oraz same parametry szablonu typu szablon nie są brane pod uwagę, chyba że dotyczą typów klasowych z przestrzenią nazw. To szczegółowe traktowanie zapewnia, że kod oparty o szablony zachowuje się zgodnie z oczekiwaniami przy korzystaniu z korzyści ADL w kontekście generycznym.

    Praktyczne zastosowania i przykłady

    Integracja obiektu ze strumieniem

    Rozważmy klasyczny program „Hello World”:

    #include <iostream>
    int main() {
      std::string str = "hello world";
      std::cout << str;
    }
    

    Ten pozornie prosty kod w całości opiera się na ADL do poprawnej kompilacji. Wyrażenie std::cout << str jest równoważne z niekwalifikowanym wywołaniem operator<<(std::cout, str). Zarówno std::cout jak i str należą do przestrzeni std. ADL analizuje obie przestrzenie, odnajdując std::operator<<(std::ostream&, const std::string&). Bez ADL konieczna byłaby rozwlekła składnia std::operator<<(std::cout, str), psująca elegancję abstrakcji strumieniowej.

    Operacje między różnymi przestrzeniami nazw

    ADL umożliwia intuicyjną współpracę typów z różnych przestrzeni nazw, gdy logicznie spełniają wspólną rolę. Wyobraźmy sobie współistniejące systemy geometryczny i pikselowy:

    namespace geometric {
      struct Point { double x,y; };
      double distance(Point p1, Point p2);
    }
    namespace pixel {
      struct Point { int x,y; };
      void draw(Point p);
    }
    void demo() {
      geometric::Point g1{1.5, 2.5}, g2{3.5, 4.5};
      pixel::Point p1{10,20};
      auto d = distance(g1, g2); // Wywołuje geometric::distance
      draw(p1); // Wywołuje pixel::draw
    }
    

    ADL rozpoznaje distance() jako geometric::distance poprzez typy argumentów, a draw() rozwiązuje poprzez przestrzeń pixel. Dzieje się tak bez użycia dyrektyw using, co pozwala utrzymać izolację przestrzeni nazw, przy zachowaniu wygodnej składni.

    Punkty dostosowań w kodzie generycznym

    Idiom std::swap doskonale obrazuje rolę ADL w programowaniu generycznym:

    namespace custom {
      struct Widget { /* złożony stan */ };
      void swap(Widget& a, Widget& b);
    }
    template<typename T>
    void process(T& a, T& b) {
      using std::swap; // swap jako domyślne
      swap(a, b); // Wywoła custom::swap jeśli jest dostępne
    }
    

    W funkcji process() niekwalifikowane wywołanie swap(a,b) używa ADL, by odnaleźć custom::swap dla custom::Widget. Deklaracja using std::swap zapewnia domyślną wersję, jeśli brak przeciążenia. To tzw. „swap two-step”, korzystający z ADL do elastycznego dostosowania algorytmów bez konieczności specjalizacji całych szablonów.

    Techniczne przypadki brzegowe i niuanse implementacyjne

    Typy niekompletne i moment rozpoznawania

    Interakcje ADL z typami niekompletnymi niosą subtelne pułapki kompilacyjne:

    struct Incomplete;
    template<typename T> struct Holder { T value; };
    void __private_foo(...) {} // Wersja domyślna
    int main() {
      Holder<Incomplete>* p = nullptr;
      __private_foo(p); // Błąd na etapie ADL
    }
    

    W tym przypadku, ADL próbuje przeanalizować Holder<Incomplete> podczas wywołania __private_foo(p). Ponieważ Incomplete nie jest kompletnym typem, kompilator nie może w pełni przeanalizować powiązanych przestrzeni, powodując błąd. Wariant z pełną kwalifikacją (::__private_foo(p)) omija ADL i działa poprawnie. Takie przypadki wymagają ostrożności przy korzystaniu z deklaracji forward w szablonach.

    Zależności parametrów szablonów

    ADL oferuje zaawansowane zachowanie dla parametryzowanych szablonów. Przykład:

    namespace nx {
      template<class P1, template<class> class P2, int P3> class X {};
    }
    namespace n1 { class A1; }
    namespace n2 {
      template<class> class A2;
      void g(X<n1::A1, A2, 5> const&);
    }
    void test() {
      nx::X<n1::A1, n2::A2, 5> obj;
      g(obj); // Rozpoznanie n2::g przez ADL
    }
    

    ADL przeszukuje:

    1. Szablon (nx);
    2. Typowe parametry szablonu (n1 z A1);
    3. Szablonowe parametry typu szablonów (n2 z A2). Parametry nietypowe i własne parametry szablonowe nie są brane pod uwagę, co zapewnia precyzyjne dopasowanie w złożonych scenariuszach.

    Zasłanianie i rozwiązywanie niejednoznaczności

    ADL potrafi nieoczekiwanie generować dodatkowych kandydatów przeciążeń:

    namespace a {
      struct X {};
      void process(X);
    }
    namespace b {
      void process(a::X);
      void demo() {
        a::X x;
        process(x); // Niejednoznaczne: a::process lub b::process
      }
    }
    

    Standardowe wyszukiwanie znajduje b::process, a ADL dodaje a::process, wywołując niejednoznaczność, mimo iż wywołanie pochodzi z b. Rozwiązanie to jawna kwalifikacja (b::process(x)) lub rozważny projekt przestrzeni nazw, by uniknąć nakładających się interfejsów.

    Krytyka i kontrowersje projektowe

    Zanieczyszczenie przestrzeni nazw

    Krytycy zarzucają, że ADL narusza hermetyzację przestrzeni nazw, czyniąc funkcje widocznymi poza ich kontekstem. Gdy dwie biblioteki definiują funkcje o tej samej nazwie dla pokrewnych typów, ADL może prowadzić do nieoczekiwanych rozpoznań:

    namespace lib1 {
      struct Data {};
      void analyze(Data);
    }
    namespace lib2 {
      struct Data {};
      void analyze(Data);
    }
    void process(lib1::Data d) {
      analyze(d); // Jednoznacznie lib1::analyze
    }
    void conflict(lib1::Data d1, lib2::Data d2) {
      analyze(d1); // lib1::analyze - OK
      analyze(d2); // lib2::analyze - OK
      analyze(d1, d2); // Niejednoznaczność przy istnieniu przeciążenia w obu bibliotekach
    }
    

    Choć przypadki z pojedynczym argumentem są jednoznaczne, przy wielu argumentach ADL może doprowadzić do niejednoznaczności i wymaga starannego planowania nazw funkcji w bibliotekach, które potencjalnie mogą współpracować.

    Wyzwania z specjalizacją szablonów

    Interakcja ADL z jawnie specjalizowanymi szablonami niosą subtelne pułapki:

    namespace utils {
      template<typename T> void process(T&);
      struct Widget {};
      template<> void process<Widget>(Widget&); // Specjalizacja dla Widget
    }
    namespace app {
      struct Widget: utils::Widget {};
    }
    void test() {
      app::Widget w;
      process(w); // Wywoła utils::process<app::Widget>, nie specjalizację
    }
    

    Tutaj specjalizacja dla utils::Widget nie jest użyta przy przetwarzaniu app::Widget. Dzieje się tak, ponieważ ADL analizuje tylko przestrzeń nazw app dla typu argumentu. Specjalizacje nie przechodzą przez dziedziczenie, co może powodować niespodziewane różnice funkcjonalne czy wydajnościowe przy typach pochodnych.

    Złożoność komunikatów o błędach

    Błędy kompilacji wiążące się z niepowodzeniem ADL często generują mało przejrzyste komunikaty. Przykład:

    namespace demo {
      struct Value {};
    }
    void calculate(demo::Value v) {
      log(v); // Brak deklaracji log
    }
    

    Komunikat w stylu 'log' was not declared in this scope nie informuje, że ADL rzeczywiście badał także przestrzeń demo. W przypadku błędu przy nazwie kwalifikowanej informacja byłaby precyzyjna (demo::log not found). Brak diagnostyki specyficznej dla ADL utrudnia debugowanie, zwłaszcza mniej doświadczonym programistom.

    Dobre praktyki efektywnego wykorzystania ADL

    Standardowy idiom swap

    Ugruntowany wzorzec implementacji dostosowywalnych operacji swap demonstruje prawidłowe wykorzystanie ADL:

    namespace custom {
      class Resource {
        int* data;
        public:
        friend void swap(Resource& a, Resource& b) {
          using std::swap;
          swap(a.data, b.data);
        }
      };
    }
    template<typename T>
    void algorithm(T& a, T& b) {
      using std::swap;
      swap(a, b); // Użyje custom::swap jeśli istnieje, w innym razie std::swap
    }
    

    To podejście dostarcza trzech kluczowych korzyści:

    1. Umożliwia optymalizację poprzez specjalizowane wersje swap dla typów użytkownika;
    2. Zapewnia generyczną wersję std::swap jako domyślną;
    3. Chroni przed rekurencyjnym wywołaniem poprzez właściwą kwalifikację nazw wewnątrz własnych implementacji.

    Funkcje wolnostojące skojarzone z typami

    ADL działa najlepiej, gdy wolnostojące funkcje deklarowane są w tej samej przestrzeni nazw co ich główny typ operandów:

    namespace geometry {
      struct Point { float x,y; };
      // Poprawnie: Funkcja w tej samej przestrzeni nazw, co typ
      float distance(Point a, Point b);
      // Problem: Funkcja w innej przestrzeni nazw
      namespace utils {
        float angle(Point a, Point b);
      }
    }
    void compute() {
      geometry::Point p1, p2;
      distance(p1, p2); // Działa przez ADL
      angle(p1, p2); // Nie działa – brak funkcji w geometry
    }
    

    Umieszczanie powiązanych funkcjonalności bezpośrednio w przestrzeni typu zapewnia naturalne odnalezienie ich przez ADL. Oddzielne, „narzędziowe” przestrzenie powinny być zarezerwowane dla funkcji pomocniczych, nie podstawowych operacji na typach.

    Strategie projektowania przestrzeni nazw

    Aby zminimalizować konflikty ADL, warto stosować następujące podejścia:

    1. Zagnieżdżone przestrzenie implementacyjne –
    
    namespace lib {
      namespace impl { // Przestrzeń unikająca ADL
        void helper();
      }
      struct Widget {
        void method() { impl::helper(); } // Jawne wywołanie
      };
    }
    
    1. Wrapery typów do rozwiązywania konfliktów –
    
    namespace lib_v2 {
      struct WidgetWrapper {
        lib_v1::Widget& inner;
        operator lib_v1::Widget&() { return inner; }
      };
      void process(WidgetWrapper); // Nowa implementacja
    }
    
    1. Izolacja przestrzeni ADL –
    
    namespace mylib_adl {
      struct Widget {};
      void process(Widget);
    }
    namespace mylib {
      using mylib_adl::Widget; // Typ do eksportu, bez funkcji process
    }
    

    Stosowanie takich wzorców pozwala kontrolować ekspozycję funkcji w ADL, zachowując pożądaną funkcjonalność.

    ADL we współczesnym C++

    Koncepcje i ograniczone ADL

    Koncepcje z C++20 zmieniają obraz ADL. Szablony ograniczone mogą zawęzić kandydatów ADL tylko do tych spełniających wymagania koncepcji:

    template<typename T> concept Drawable = requires(T t) { { draw(t) } -> std::same_as<void>; };
    template<Drawable T> void render(T obj) { draw(obj); /* ADL uwzględnia tylko draw spełniające koncept */ }
    

    Taki ograniczony lookup zapobiega rozważaniu niepożądanych funkcji, jeśli nie spełniają interfejsu, co podnosi bezpieczeństwo i czytelność generowanego kodu.

    Współpraca z korutynami

    Nowoczesne implementacje korutyn odkrywają niuanse ADL:

    namespace winrt { template<typename...> struct coroutine_traits; }
    namespace std { template<typename...> struct coroutine_traits; }
    winrt::IAsyncAction async_func() { co_await winrt::resume_background(); }
    

    Kompilowane jako C++17 z std::experimental::coroutine_traits, ADL bada tylko przestrzeń experimental. Migracja do C++20 oraz std::coroutine_traits rozszerza przeszukiwanie na pełną przestrzeń std, co może powodować kolizje, jeśli znajdą się tam nieprzewidziane funkcje (np. invoke).

    Porównania trójargumentowe (three-way comparison)

    Operator <=> wprowadza zagadnienia ADL:

    namespace demo {
      struct Value {
        int data;
        auto operator<=>(const Value&) const = default;
      };
      void compare(Value a, Value b) {
        a < b; // Użycie operatora domyślnego, bez ADL
        (a <=> b) < 0; // Równoważne jawne wywołanie
      }
    }
    

    Operator używa ADL, gdy wywołujemy jawnie operator<=> na argumentach, natomiast implementacje domyślne pozostają w zakresie klasy i nie wprowadzają dodatkowych zależności przestrzeni nazw.

    Podsumowanie

    Argument-dependent lookup to zaawansowany mechanizm rozpoznawania nazw, głęboko zintegrowany z tożsamością C++. Jego projekt jest efektem kompromisu między hermetyzacją przestrzeni nazw a wygodą składni, umożliwiając naturalną ekspresję operacji na typach niestandardowych oraz wzorce programowania generycznego. Słynny przykład std::cout << obj pokazuje niezbędną rolę ADL w kreowaniu ekspresywnych API, podczas gdy punkty dostosowań, jak std::swap, pozwalają na specjalizację algorytmów bez konieczności głębokich hierarchii dziedziczenia.

    Mimo korzyści, ADL wprowadza znaczną złożoność do dużych projektów. Konflikty przestrzeni nazw, ograniczenia specjalizacji szablonów oraz niejasne komunikaty o błędach zmuszają do stosowania rygorystycznych strategii projektowych, jak „izolacja przestrzeni ADL” czy przemyślane rozmieszczanie funkcji zaprzyjaźnionych. Nowoczesne C++, takie jak koncepcje czy korutyny, dodają kolejne niuanse i wymagają głębszej wiedzy o regułach wyszukiwania.

    Opanowanie ADL wymaga zrozumienia mechanizmu dwutorowego wyszukiwania, rozpoznawania sytuacji, w których jawna kwalifikacja zapobiega niejednoznaczności oraz stosowania konsekwentnych wzorców umieszczania funkcji powiązanych z typami. Poprawnie używany ADL pozostaje potężnym narzędziem do tworzenia ekspresywnych, typobezpiecznych interfejsów, współpracujących zarówno z własnymi typami, jak i biblioteką standardową, wpisując się w filozofię C++ dającą twórcom niskopoziomową kontrolę bez rezygnacji z wysokopoziomowych abstrakcji. Przyszły rozwój języka prawdopodobnie będzie dalej dopracowywać przypadki brzegowe ADL, zachowując jego kluczową rolę w projektowaniu eleganckiego, wydajnego programowania generycznego.

    Polecane:

    • Wzorzec PImpl
    • RAII w C++ – zbiór najlepszych praktyk
    • Historia wyrażeń lambda w C++ od C++03 do C++20
    • Przegląd języka C++ – co nowego w standardach od C++11 do C++23
    • Skrajnie niepotrzebne, skrajne przypadki w C++
    Share. Facebook Twitter LinkedIn Email Copy Link
    Oskar Klimkiewicz
    • Website

    Inżynier oprogramowania specjalizujący się w C++, absolwent Wydziału Elektroniki i Technik Informacyjnych Politechniki Warszawskiej. Od ponad 8 lat projektuje i rozwija systemy o wysokiej dostępności, głównie dla branży fintech i IoT. PS. Zdjęcie wyretuszowane przez AI :)

    Podobne artykuły

    Maszyna stanów oparta o std::variant

    8 Mins Read

    std::deque w C++ – kiedy wybrać dwukierunkową kolejkę zamiast vectora

    4 Mins Read

    Tablice w C++ od podstaw – deklaracja, inicjalizacja, iteracja i typowe pułapki

    4 Mins Read
    Leave A Reply Cancel Reply

    Oglądaj, słuchaj, ćwicz - zdobywaj nowe umiejętności online
    Nie przegap

    Kodeki audio i wideo – paczki niezbędne do odtwarzania filmów

    Oskar Klimkiewicz6 Mins Read

    W dzisiejszym świecie multimediów, gdzie filmy, seriale i klipy wideo zalewają internet oraz platformy streamingowe,…

    Programy antyplagiatowe – jak sprawdzić unikalność tekstu?

    2026-05-17

    Jak przenieść pliki z tabletu na pendrive? Podłączanie i transfer danych

    2026-05-16

    CDex – zgrywanie muzyki z płyt CD Audio do formatu MP3

    2026-05-15
    Social media
    • Facebook
    • Twitter
    • LinkedIn
    O nas
    O nas

    CPP Polska to serwis internetowy poświęcony technologii, programowaniu, IT, biznesowi i finansom. Znajdziesz tu porady, wskazówki i instrukcje dla wszystkich czytelników IT & Tech & Biz.

    Facebook X (Twitter) LinkedIn RSS
    Najnowsze

    Kodeki audio i wideo – paczki niezbędne do odtwarzania filmów

    2026-05-18

    Programy antyplagiatowe – jak sprawdzić unikalność tekstu?

    2026-05-17

    Jak przenieść pliki z tabletu na pendrive? Podłączanie i transfer danych

    2026-05-16
    Popularne

    Skrajnie niepotrzebne, skrajne przypadki w C++

    2025-06-28

    Wyszukiwanie testów w Google Test – metody i narzędzia

    2025-06-28

    Czy C jest wolniejszy od C++? Zero-cost abstraction w praktyce

    2025-06-28
    © 2026 CPP Polska. Wszelkie prawa zastrzeżone.
    • Lista publikacji
    • Współpraca
    • Kontakt

    Type above and press Enter to search. Press Esc to cancel.