Limbajul C++ a fost descris de către Scott Meyers ca fiind o federație de limbaje înrudite. Fiecare dintre aceste sublimbaje vine cu propriul set de reguli și mod de a scrie lucrurile, iar pentru a le aprofunda, fiecare trebuie privit din alt unghi și studiat cu o altă mentalitate. El a identificat patru astfel de limbaje: C, C++ orientat pe obiecte, C++ cu template-uri și STL. C++ poate fi un limbaj care intimidează prin complexitatea acestuia: atunci când scriem cod, trebuie să ținem cont de toate sublimbajele și paradigmele aflate în legătură cu acest limbaj. De asemenea, C++ este în continuă schimbare, pentru că o nouă iterație își face apariția o dată la trei ani, când apar noi concepte pe care este necesar să le avem în vedere. În acest fel, monstrul C++ devine și mai mare și fioros. De la introducerea lor, template-urile au avut și ele parte de acest tratament: template-uri variadice în C++11, lambda-uri templetizate în C++14, etc. Standardul C++20 nu va fi excepție și va aduce și el noi surprize, printre care "Conceptele".
Template-urile sunt mecanismul prin care putem scrie cod generic în C++. O diferență majoră față de genericele din C# este că template-urile sunt deduse la compile time și astfel scăpăm de overheadul cauzat de deducerea tipurilor în timpul execuției. Se aseamănă mai mult cu genericele din Java și mecanismul de type erasure, însă nu sunt atât de rigide. De asemenea, în C++ putem folosi variabile pentru parametrii unui template. Template-urile sunt astfel foarte puternice, metaprogramarea cu template-uri în C++ fiind turing complete. Putem scrie întregi algoritmi care să ruleze la compile time, ca de exemplu, calcularea numerelor Fibonacci:
template
constexpr int fibonacci = fibonacci
+ fibonacci;
template<>
constexpr int fibonacci<0> = 0;
template<>
constexpr int fibonacci<1> = 1;
Parametrii de tip ai template-urilor acceptă orice. Din cauza acestei libertăți pot interveni probleme atunci când dăm un tip care nu este disponibil unei funcții sau clase templetizate. De exemplu, ce se întâmplă când apelăm std::sort
pe un container de elemente al căror tip nu definește operatori de comparație? Compilerul va ajunge la un moment dat la o expresie de genul if (element1 < element2)
și ne va da multe erori:
error: C2672: 'operator __surrogate_func':
no matching overloaded function found
error: C2893: Failed to specialize function template 'unknown-type std::less::operator ()
(_Ty1 &&,_Ty2 &&) const'
... și multe altele ...
Pentru că nu am specificat un functor pentru comparație, cel implicit, std::less
, este folosit. std::less
compară cele două argumente folosind operatorul "<", care nu este definit pentru tipul nostru. Aceste mesaje de eroare pot intra în detalii foarte tehnice care nu sunt foarte evidente. Mai mult, cu toate că acestea au semnificație pentru compilator, unui programator îi pot da bătăi de cap.
Tocmai de aceea, în C++20 vor fi introduse conceptele. Acestea vor permite programatorului să definească categorii semantice ce au o semnificație pentru el, din care pot face parte parametrii template-urilor. Altfel spus, cu ajutorul conceptelor ne putem defini constrângeri pentru template-uri și să le grupăm sub un nume care are pentru programator o semnificație anume. Putem privi conceptele ca fiind echivalentul interfețelor, dar pentru argumentele template-urilor. În contextul exemplului anterior, putem identifica o astfel de categorie ce conține tipurile comparabile cu operatorul "<". Această categorie ar putea fi scrisă drept concept în C++ astfel:
template
concept Comparable = requires(T t1, T t2) {
{ t1 < t2 } -> bool;
};
În interiorul clauzei "requires", este specificat că, având două obiecte t1 și t2 de tip T, expresia "t1 < t2" trebuie să compileze și returnează un tip ce poate fi convertit în mod implicit la "bool". O funcție a cărei parametru de template trebuie să adere la conceptul "Comparable" poate fi declarată într-unul din următoarele moduri:
template
compare(T t1, T t2) {
...
}
template requires Comparable
compare(T t1, T t2) {
...
}
În cazul în care tipurile argumentelor nu vor respecta conceptul "Comparator" (vom folosi tipul "Foo"), erorile ne vor ajuta să suferim mult mai puțin:
error: cannot call compare with 'Foo' and 'Foo'
note: concept 'Comparable' was not satisfied
Alternativ, pentru std::sort, putem avea un concept "Comparator" pentru functorul de comparație, care trebuie să aibă definit operatorul "( )", ce are doi parametri de tipul elementelor din container și returnează un boolean.
template
concept Comparator = requires(F f, It i1, It i2) {
{ f(*i1, *i2) } -> bool;
};
Acum, declarația funcției std::sort, constrânsă cu conceptul "Comparator", devine:
template
requires Comparator
sort(Iterator begin, Iterator end, Compare comp);
Conceptele mai pot fi utilizate și atunci când specializăm funcțiile sau clasele templetizate. Compilatorul va putea alege varianta template-ului ce satisface proprietățile tipului definite de către concept. Putem astfel să tratăm diferit anumite categorii de tipuri, atunci când un tip îndeplinește anumite condiții de care ne putem folosi pentru a implementa pentru acesta, o variantă specializată și mai rapidă a unui algoritm sau dorim pur și simplu alt comportament. De exemplu, un counting sort e una dintre cele mai bune metode de a sorta numere întregi mici, pentru care putem scrie conceptul:
template
SmallInteger = requires {
sizeof(T) == 1;
std::is_integral::value;
};
template sort(T* begin, T* end) {
// Counting sort
}
Sau, dacă valorile sortate sunt foarte mari, e mai eficient să construim o listă separată de pointeri, să sortăm pointerii, apoi să construim din nou lista sortată de elemente:
template
LargeElement = requires {
sizeof(T) > 8;
};
Putem obține un comportament asemănător conceptelor și în versiunile existente ale limbajului, cu ajutorul "std::enable_if", însă sintaxa este mult mai urâtă, iar acele două condiții sunt mai puțin evidente decât "SmallInteger".
template
typename std::enable_if::value>::type
sort(T lhs, T rhs)
{
// Counting sort
}
Sunt multe de spus despre concepte. Cei pasionați pot intra pe cppreference pentru mai multe detalii tehnice. Use case-urile nu sunt atât de multe pentru programarea în C++ de zi cu zi și cel mai mult i-ar interesa pe cei care scriu biblioteci. Totuși, nu va trebui să folosim acest feature ca acesta să ne ușureze munca, căci vom avea o versiune de Standard Template Library ce va folosi concepte. Atunci când un parametru de template al unei funcții sau clase din STD nu poate fi dedus, în locul unui mesaj de eroare obscur, compilatorul ne va putea spune pur și simplu că un anumit concept nu este compatibil cu argumentele date.