ConceptC++: Koncepty

Koncepty

Przykład konceptu

Przykład: sortowanie

Rozważmy następujący generyczny kod sortowania:
template<typename T>
void sort(T data[], int n){
  bool done=false;
  while(!done){
    done = true;
    for(int j=0;j<n-1;++j){
      if(data[j+1]<data[j]){
        T t=data[j];
        data[j]=data[j+1];
        data[j+1]=t;
        done=false;
      }
    }
  }
}
oraz klasę Pair
template<typename T>
class Pair{
  private:
    T p,q;
  public:
    Pair(const T& A_p,const T& A_q):p(A_p),q(A_q){}
    friend bool operator>(const Pair &r1,const Pair &r2){
      return r1.p==r2.p ? r1.q<r2.q : r1.p<r2.p;
    }
    friend std::ostream &operator<<(std::ostream &out,const Pair &r){
      out << "(" << r.p << "," << r.q << ")";
      return out;
    }
};
Jeśli pomylimy się (jak wyżej) i klasę tę zamiast w operator < wyposażymy w operator > to przy kompilacji programu
int main(){
  typedef Pair<int> pr;

  int size=6;
  pr data[]={pr(5,3),pr(2,3),pr(1,3),pr(17,25),pr(17,22),pr(5,6)};
  sort(data,size);
  for(int i=0;i<size;++i) std::cout << data[i] << std::endl;
}
otrzymamy błąd kompilacji
no match for 'operator<' in
'*(data + ((((unsigned int)j) + 1u) * 8u)) < *(data + ((unsigned int)(((unsigned int)j) * 8u)))'
W małym programie, stosunkowo łatwo się zorientować, gdzie zabrakło operatora <, ale w dużym programie tego typu komunikaty o błędach potrafią zajmować kilkanaście linii i więcej i wtedy robi się z tego poważny problem.

Szablon funkcji sortującej

By temu zaradzić, należałoby w definicji szablonu sort wyspecyfikować wymagania odnośnie typu T tak by kompilator od razu mógł sprawdzić, czy dostarczony typ Pair<int> spełnia te wymagania.

Przykład: sumowanie

Ratunek: koncepty

Definiujemy koncept
auto concept LessThanComparable<typename T> {
  bool operator<(const T&, const T&);
}
a w definicji szablonu sort zamieniamy typname T na LessThanComparable T. Kompilujemy ConceptGcc i dostajemy błędy:
sortc.cpp: In function 'void sort(T*, int)':
sortc.cpp:8: error: constructor 'T'::T'(const T&)' is inaccessible
sortc.cpp:9: error: no match for 'operator=' in 'data[j] = data[(j + 1)]'
sortc.cpp:10: error: no match for 'operator=' in 'data[(j + 1)] = t'
sortc-pair.cpp: In function 'int main()':
sortc-pair.cpp:17: error: no matching function for call to 'sort(main()::pr [6], int&)'
sortc.cpp:2: note: candidates are: void sort(T*, int) [with T = main()::pr] 
sortc-pair.cpp:17: note:   no concept map for requirement 'LessThanComparable >'
Błędy w funkcji main związane są z pomylonym kierunkiem operatora < w klasie Pair<int>, a w szczególności z ostatniej linii jasno wynika co jest nie tak. Błędy w funkcji sort pokazują, że w istocie wykorzystujemy więcej założeń o typie T niż to wynika z konceptu LessThanComparable. Potrzebujemy dodać dodatkowe koncepty:
auto concept Assignable<typename T> {
  T& operator=(T&, const T&);
}
auto concept CopyConstructible<typename T> {
  T::T(const T&);
}
i poinformować o tym kompilator zmieniając nagłówek szablonu funkcji sort:
template<LessThanComparable T>
requires Assignable<T>,CopyConstructible<T>
void sort(T data[], int n){
....
}

Kompletny przykład : sortowanie

Przykład: sumowanie

Koncepty w bibliotece standardowej ConceptC++

Biblioteka standardowa ConceptC++ dostarcza szereg użytecznych konceptów w przestrzeni nazw std By z nich korzystać należy dokonać włączenia:
#include <concepts>
Oto przykład:
  auto concept CopyConstructible<typename T>
  {
    T::T(const T&);
  }

Koncept iteratora

Rozszerzmy nasz przykład algorytmu sortowania tak, by działał nie tylko z tablicami, ale z dowolnymi kolekcjami udostępnianymi poprzez sekwencyjne pojemniki. W tym celu definiujemy koncept iteratora
concept ForwardIterator<typename Iter>{
  typename value_type;

  Iter::Iter(const Iter&);
  Iter& operator++(Iter& x);

  value_type& operator*(Iter);

  bool operator==(Iter, Iter);
  bool operator!=(Iter, Iter);
};
Przykład ten w szczególności ilustruje, że koncept może wymagać obecności typu skojarzonego. Przykład ten nie został zdefiniowany jako auto, gdyż uznajemy, że jest zbyt duże ryzyko istnienia klasy, która udostępnia wszystkie wymagane składniki i metody jednak ich semantyka jest inna niż oczekiwana.

Mapowanie konceptów

Zamiast tego informujemy kompilator bezpośrednio jakie typy spełniają oczekiwane wymagania, wykorzystując t.zw. concept_map.
concept_map ForwardIterator<double*> {
  typedef double value_type;
};
Można to zrobić ogólniej dla wszystkich wskaźników poprzez podanie szablonu mapowania.
template<typename T>
concept_map ForwardIterator<T*>{
  typedef T value_type;
};
template<ForwardIterator Iter>
requires Assignable<Iter::value_type>,
CopyConstructible<Iter::value_type>,
LessThanComparable<Iter::value_type>
void sort(Iter first, Iter afterLast){
  bool done=false;
  while(!done){
    done = true;
    Iter i=first,in=first;
    ++in;
    for(;in!=afterLast;++i,++in){
      if(*in<*i){
        Iter::value_type t=*i;
        *i=*in;
        *in=t;
        done=false;
} } } }

Kompletny przykład : sortowanie

Przykład: sumowanie

Retroaktywne modelowanie

Zdarza się, że klasa którą chcemy wykorzystać przy konkretyzacji typu w abstrakcyjnym algorytmie co prawda implementuje potrzebną metodę, ale metoda ta inaczej się w tej klasie nazywa. Możemy oczywiście dodać stosowne "opakowanie" do tej klasy, ale nie zawsze chcemy modyfikować istniejącą klasę tylko dlatego, że chcemy jej używać w abstrakcyjnym algorytmie. Rozwiązaniem jest retroaktywne modelowanie oparte o stosowne mapowanie konceptów.
concept Stack<typename X> {
  typename value type;
  void push(X&, const value type&);
  void pop(X&);
  value type top(const X&);
  bool empty(const X&);
};
Klasa wektor ma wszystko co potrzeba do traktowania jej jako implementacji stosu, ale nie wszystkie nazwy się zgadzają. Zgodność nazw otrzymujemy przez mapowanie:
template<typename T>
concept map Stack<std::vector<T> >{
  typedef T value type;
  void push(std:: vector& v, const T& x) { v.push back(x); }
  void pop(std:: vector& v) { v. pop back(); }
  T top(const std:: vector& v) { return v. back(); }
  bool empty(const std::vector& v) { return v. empty(); }
};

Koncepty wieloargumentowe

Czasami przydają się koncepty wieloargumentowe
auto concept Addable<typename T, typename U = T> {
  T operator+(T x, U y);
};
Po takim rozszerzeniu możemy uogólnić nasz algorytm sumujący tak, by typ w którym gromadzimy wynik był odmienny od typu sumowanego.
template<ForwardIterator Iter, Assignable T>
requires Addable<T, value_type>
T sum(Iter first, Iter last, T result)
{
  for (; first != last; ++first)
    result = result + *first;
  return result;
}