3. Koncepty

3.2. Koncepty

W kontekście programowania z użyciem szablonów, koncepty stają się niezbędnym narzędziem, umożliwiającym programiście narzucanie oczekiwań co do funkcjonalności i zachowań dla typów, które są używane z danym szablonem. Koncepty pozwalają na wyraźne i zwięzłe zdefiniowanie warunków, które muszą być spełnione przez dany typ, aby mógł być użyty z danym szablonem.

Abstrakcyjne Definicje Poprzez Koncepty

Koncepcja konceptów umożliwia tworzenie abstrakcyjnych definicji, zwanych konceptami, które są niezależne od konkretnego szablonu. W praktyce, koncept to zestaw warunków, które muszą być spełnione przez dany typ, aby był uznany za model konceptu. Ten model konceptu może być potem używany z różnymi szablonami, co pozwala na tworzenie generycznych algorytmów i struktur danych.

Podobnie jak w hierarchii dziedziczenia, koncepty mogą tworzyć hierarchie. To oznacza, że jeden koncept może być bardziej ogólny, obejmując szeroki zakres typów, podczas gdy inny może być bardziej szczegółowy, zdefiniowany dla konkretnego podzbioru typów. Takie hierarchie konceptów pozwalają na elastyczne korzystanie z ogólnych definicji i jednocześnie narzucanie bardziej konkretnej specyfikacji, gdy jest to konieczne.

Ograniczenia konceptów: prawidłowe wyrażenia, typy stowarzyszone i semantyka

Ograniczenia konceptów są zbiorem warunków, które muszą być spełnione przez dany typ. Obejmują one różnorodne aspekty:

  1. Prawidłowe wyrażenia: Określenie, jakie operacje i wyrażenia muszą być poprawnie obsługiwane przez dany typ w ramach danego konceptu.
  2. Typy stowarzyszone: Dodatkowe typy, które występują w kontekście prawidłowych wyrażeń i operacji na danym typie.
  3. Semantyka: Definicje znaczenia wyrażeń, które są zawsze prawdziwe dla danego konceptu. To pozwala na określenie oczekiwanego zachowania typów związanych z danym konceptem.

Ograniczenia konceptów mogą również obejmować informacje dotyczące złożoności algorytmów. Gwarancje co do czasu i innych zasobów potrzebnych do wykonania danego wyrażenia są istotne w kontekście efektywności i optymalizacji. Programiści, korzystając z konceptów, mogą wybierać takie, które są zarówno ogólne, jak i wydajne, co pozwala na tworzenie generycznych rozwiązań, które są jednocześnie efektywne dla różnych typów danych.

W skrócie, koncepty w programowaniu uogólnionym stanowią koncepcję kluczową dla definiowania oczekiwań i ograniczeń dla typów używanych z szablonami. Odpowiednie zastosowanie konceptów pozwala na tworzenie generycznych i elastycznych rozwiązań, które jednocześnie pozostają efektywne i zgodne z oczekiwaniami programisty.

Przykład

Przyjrzyjmy się omawianemu wcześniej szablonowi funkcji max


template<typename T> max(T a,T b) {return (a>b)?a:b;}

Jakie koncepty możemy tu odkryć?

Gramatyka

Jakie warunki musi spełniać typ T, aby podstawienie go jako argument szablonu max dawało poprawne wyrażenie? Musi mieć zdefiniowany operator porównania bool operator>(...). Nie jest ważna sygnatura tego operatora. Nie ma znaczenia jak parametry są przekazywane, czy operator jest w klasie, czy jako funkcja, itp... jedno co ważne to że jeśli x i y są obiektami typu T to wyrażenie:


    x>y

jest poprawne (skompiluje się).

Łatwiej jest przeoczyć fakt, że ponieważ argumenty wywołania są zwracane i przekazywane przez wartość, to typ T musi posiadać konstruktor kopiujący. Oznacza to, że jeśli x i y są obiektami typu T to wyrażenia:


    T(x);
    T x(y);
    T x = y;

są poprawne.

Spełnienie obydwu tych warunków zapewni nam poprawność gramatyczną wywołania szablonu z danym typem, tzn. kod się skompiluje.

Semantyka

Mogłoby się wydawać, że jest bez znaczenia jak zdefiniujemy operator>(...). Koncept typu T jest jednak częścią kontraktu dla funkcji max. Kontrakt stanowi, że jeżeli użytkownik dostarczy do funkcji argumenty o typach zgodnych z konceptem i o wartościach spełniających być może inne warunki wstępne, to twórca funkcji gwarantuje, że zwróci ona poprawny wynik.

Z definicji maksimum żaden element argument funkcji max nie może być większy od wyniku, czyli wyrażenie

musi być zawsze prawdziwe. Jasne jest, że jeśli dla jakiegoś typu X zdefiniujemy operator porównania tak, aby zwracał zawsze prawdę lub aby był równoważny operatorowi równości to wyrażenie nie może być prawdziwe dla żadnej wartości a i b. Musimy narzucić pewne ograniczenia semantyczne na operator>() - żądanie, aby relacja większości definiowana przez ten operator była relacją porządku częściowego:

To rozumowanie można by ciągnąć dalej i zauważyć, że nawet z tym ograniczeniem uzyskamy nieintuicyjne wyniki w przypadku, gdy obiekty a i b będą nieporównywalne, tzn. !(a>b) i !(b>a).

Dostaliśmy zbiór warunków, które musi spełniać typ T, aby móc go podstawić do szablonu funkcji max. Jak go nazwać?

Comparable - ale istnienie konstruktora kopiującego nie ma z tym nic wspólnego. Próbujemy upchnąć dwa niezależne pojęcia do jednego worka. Co więcej bardzo łatwo jest zrezygnować z konieczności posiadania konstruktora kopiującego, zmieniając deklarację max na:


    template<typename T> const T& max(const T&,const T&);

Teraz argumenty i wartość zwracana przekazywane są przez referencję i nie ma potrzeby kopiowania obiektów. Logiczne jest wydzielenie dwu konceptów: jednego definiującego typy porównywalne, drugiego - typy "kopiowalne". Dalej możemy zauważyć, że istnienie operatora > automatycznie pozwala na zdefiniowanie operatora < poprzez:


    bool operator<(const T& a,const T&b) {return b>a;};

Podobnie istnienie konstruktora kopiującego jest blisko związane z istnieniem operatora przypisania.

Finalnie - najlepiej zapewne będzie wydzielić dwa koncepty:

  • Comparable - obiekty można porównywać za pomocą operatorów < i >
  • Assignable - obiekty możemy kopiować i przypisywać do siebie.