Concepturi
Începând cu standardul C++20, limbajul C++ a introdus o funcționalitate numită concepts (concepturi). Concepturile permit stabilirea de restricții pentru parametrii șabloanelor (atât pentru șabloanele funcțiilor, cât și pentru șabloanele claselor).
Un concept reprezintă, de fapt, un șablon pentru un set numit de restricții, unde fiecare restricție impune una sau mai multe cerințe pentru unul sau mai mulți parametri ai șablonului. În general, are următoarea formă:
template <parametri>
concept nume_concept = restricții;
Lista de parametri ai unui concept conține unul sau mai mulți parametri ai șablonului. În timpul compilării, compilatorul evaluează conceptele pentru a determina dacă setul de argumente respectă restricțiile impuse.
Restricțiile reprezintă expresii condiționale care returnează un rezultat de tip bool – dacă parametrul de tip satisface condiția, atunci se returnează true.
Exemplu simplu:
template <typename T>
concept size = sizeof(T) <= sizeof(int);
În acest caz, este definit conceptul size. Acesta presupune că tipul care va fi transmis prin parametrul T trebuie să respecte condiția sizeof(T) <= sizeof(int). Cu alte cuvinte, dimensiunea fizică a obiectelor de tipul T nu trebuie să fie mai mare decât dimensiunea unui obiect de tip int.
Pentru a verifica dacă un tip specificat respectă un concept, se folosește expresia conceptului, care constă din numele conceptului, urmat de parametrii șablonului în paranteze unghiulare:
nume_concept<valori_pentru_parametri>
De exemplu, să verificăm conceptul size definit mai sus:
#include <iostream>
template <typename T>
concept size = sizeof(T) <= sizeof(int);
int main()
{
std::cout << std::boolalpha << size<unsigned int> << std::endl; // true
std::cout << std::boolalpha << size<char> << std::endl; // true
std::cout << std::boolalpha << size<double> << std::endl; // false
}
În acest caz, expresiile size<unsigned int> și size<char> respectă restricțiile conceptului, deoarece tipurile unsigned int și char nu sunt mai mari decât tipul int. De aceea, aceste expresii vor returna true. Expresia size<double> nu respectă restricția conceptului, deoarece dimensiunea tipului double este mai mare decât cea a tipului int, iar această expresie va returna false.
Unele concepte pot fi construite pe baza altor concepte. De exemplu:
#include <iostream>
template <typename T>
concept small_size = sizeof(T) < sizeof(int);
template <typename T>
concept big_size = sizeof(T) > sizeof(long);
template <typename T>
concept size = small_size<T> || big_size<T>;
int main()
{
std::cout << std::boolalpha << size<unsigned int> << std::endl; // false
std::cout << std::boolalpha << size<char> << std::endl; // true
std::cout << std::boolalpha << size<double> << std::endl; // true
}
În primul rând, sunt definite două concepte simple. Conceptul small_size cere ca dimensiunea tipului să fie mai mică decât dimensiunea tipului int. Conceptul big_size cere ca dimensiunea tipului să fie mai mare decât dimensiunea tipului long. Folosind operatorii logici standardi && și ||, putem combina aceste restricții. Astfel, conceptul size cere ca tipul T să respecte fie condiția small_size<T>, fie condiția big_size<T>:
concept size = small_size<T> || big_size<T>;
Acum, întrebarea principală este: de ce avem nevoie de aceste concepte? Putem folosi concepte ca restricții pentru șabloane:
#include <iostream>
template <typename T>
concept size = sizeof(T) <= sizeof(int);
template <typename T> requires size<T> // folosim conceptul size ca restricție
T sum(T a, T b) { return a + b; }
int main()
{
std::cout << sum(10, 3) << std::endl; // 13
//std::cout << sum(10.6, 3.7) << std::endl; // ! Eroare
}
În acest caz, este definit conceptul size, conform căruia dimensiunea tipului T trebuie să fie mai mică sau egală cu dimensiunea tipului int.
La definirea șablonului funcției sum, se aplică restricția cu ajutorul conceptului size:
template <typename T> requires size<T>
Adică valorile care vor fi transmise acestei funcții trebuie să reprezinte un tip al cărui dimensiune nu este mai mare decât dimensiunea tipului int. Astfel, putem apela această funcție, transmitem valori de tip int:
sum(10, 3)
Dar nu putem transmite valori de tip double, deoarece aceste valori ocupă 8 byte în memorie, ceea ce este mai mult decât valorile de tip int:
sum(10.6, 3.7)
De aceea, ultima linie din cod este comentată. Dacă am decomentat-o, am primi o eroare de compilare.
Sintaxa prescurtată
C++ permite prescurtarea sintaxei de aplicare a conceptului:
template <size T> // în acest caz se folosește conceptul size
T sum(T a, T b) { return a + b; }
În acest caz, conceptul se specifică între unghiile <> înainte de numele parametrului de tip, în loc de cuvântul typename.
Concepturi încorporate
Aceste concepturi nu au un mare sens în sine și sunt destinate pentru a oferi o înțelegere generală despre cum se definesc și cum funcționează concepturile. În plus, în biblioteca standard C++ există un set destul de mare de concepte încorporate, care pot fi utilizate în diverse situații. Toate aceste concepte sunt definite în modulul <concepts>.
De exemplu, conceptul încorporat std::same_as<K, T> verifică dacă T și K reprezintă același tip. De exemplu, să presupunem că trebuie să definim un șablon de funcție care adună numere de tip int și double:
#include <iostream>
#include <concepts>
template <typename T>
concept sum_types = std::same_as<T, int> || std::same_as<T, double>;
template <sum_types T>
T sum(T a, T b) { return a + b; }
int main()
{
std::cout << sum(10, 3) << std::endl; // 13
std::cout << sum(10.6, 3.2) << std::endl; // 13.8
}
Aici, restricția:
std::same_as<T, int> || std::same_as<T, double>
stabilește că tipul T trebuie să reprezinte fie int, fie double.
În acest caz, am fi putut folosi și alt concept — std::convertible_to<K, T>. Acesta verifică dacă un tip K poate fi convertit într-un tip T. De exemplu, valoarea de tip int poate fi convertită implicit într-un double. Astfel, am putea rescrie exemplul anterior astfel:
#include <iostream>
#include <concepts>
template <typename T>
concept sum_types = std::convertible_to<T, double>; // T trebuie să poată fi convertit în double
template <sum_types T>
T sum(T a, T b) { return a + b; }
int main()
{
std::cout << sum(10, 3) << std::endl; // 13
std::cout << sum(10.6, 3.2) << std::endl; // 13.8
}
În acest caz, restricția conceptului sum_types va permite atât int, cât și double, pentru că ambele tipuri pot fi convertite în double.