Sari la conținutul principal

Pentru a demonstra puterea de expresie a limbajului C++ vom lua un exemplu trivial din algebră, o magmă.

O magmă este definită ca un tuplu (S,)(S, \otimes) unde SS este o mulțime și \otimes este o operație binară :S×SS\otimes : S \times S \rightarrow S.

În C++20, această structură matematică se traduce foarte simplu și elegant ca:

#include <concepts>

namespace sayten
{
template<typename TypeT>
concept HasCrossOperation =
requires (const TypeT &value_a, const TypeT &value_b)
{
{ value_a.cross(value_b) } -> std::same_as<TypeT>;
};

template<typename TypeT>
concept IsMagma = HasCrossOperation<TypeT>;

template<IsMagma TypeT>
constexpr TypeT call_cross(const TypeT &value_a, const TypeT &value_b)
{
return value_a.cross(value_b);
}

template<typename TypeT, TypeT cross_operation(const TypeT&, const TypeT&)>
struct magma
{
const TypeT value;

explicit magma(const TypeT &value) : value(value) {}

constexpr magma cross(const magma &other) const
{
return magma(cross_operation(value, other.value));
}

explicit constexpr operator TypeT() const noexcept
{
return value;
}

constexpr bool operator==(const magma &other) const
requires std::equality_comparable<TypeT>
{
return value == other.value;
}

constexpr bool operator==(const TypeT &other) const
requires std::equality_comparable<TypeT>
{
return value == other;
}

constexpr bool operator!=(const magma &other) const
requires std::equality_comparable<TypeT>
{
return value != other.value;
}

constexpr bool operator!=(const TypeT &other) const
requires std::equality_comparable<TypeT>
{
return value != other;
}
};
}

Concepte

Ca să explicăm codul de mai sus, mai întâi trebuie să înțelegem ce sunt conceptele (concepts) în C++. Conceptele au apărut oficial în C++20, deși existau ca specificații tehnice înainte de apariția lor în standard.

Un concept este un predicat la compile-time, lucru ce înseamnă că acestea sunt evaluate în orice context constant. Asta înseamnă că pot fi folosite ca constrângeri pentru parametrii de template și pot fi evaluate în expresii constante, ca de exemplu în constexpr if.

Un concept poate evalua o proprietate a parametrilor generici. De exemplu, în biblioteca standard există conceptul std::same_as care verifică dacă două tipuri date ca parametri sunt la fel sau nu. De exemplu, codul următor:

if constexpr (std::sane_as<int, int>) 
{
std::cout << "Types int and int are the same\r\n";
}
else
{
std::cout << "Types int and int are not the same\r\n";
}

if constexpr (std::sane_as<int, std::string>)
{
std::cout << "Types int and std::string are the same\r\n";
}
else
{
std::cout << "Types int and std::string are not the same\r\n";
}

Va fi evaluat ca:

std::cout << "Types int and int are the same\r\n";
std::cout << "Types int and std::string are not the same\r\n";

Deja se poate vedea avantajul conceptelor în C++ față de folosirea obișnuită a macro-urilor. Dar mergem un pas înainte pe exemplul dat mai sus. Conceptul HasCrossOperation definit aici testează un tip de date dacă respectă o condiție, condiția fiind ca tipul să aibă o metodă numită cross care să reprezinte operația binară din magma și pentru oricare două valori de tipul dat această metodă să întoarcă același tip.

template<typename TypeT>
concept HasCrossOperation =
requires (const TypeT &value_a, const TypeT &value_b)
{
{ value_a.cross(value_b) } -> std::same_as<TypeT>;
};

Orice concept poate fi evaluat folosind alte concepte. Conceptul IsMagma va evalua un tip dacă este o magmă. Pentru că structurile și clasele în C++ pot avea metode, vom pune condiția doar ca tipul să satisfacă condiția HasCrossOperation, deși putem folosi și alte concepte cu operatorii logici && și ||. În exemplul nostru, dacă un tip de date este în sine o mulțime de valori și dacă are asociată o metodă pe post de operație binară, deja avem o magmă.

template<typename TypeT>
concept IsMagma = HasCrossOperation<TypeT>;

Alternativ, dacă noi vrem operația respectivă să o apelăm infixată și nu infixată, putem declara și o funcție cu constrângerea menționată.

template<IsMagma TypeT>
constexpr TypeT call_cross(const TypeT &value_a, const TypeT &value_b)
{
return value_a.cross(value_b);
}

Aici am creat o funcție generică care să restricționeze tipul generic doar la cele care satisfac condiția de magma.

Cu toate acestea, vrem să capturăm complet conceptul de magma care, din definiție, este un tuplu. Putem exprima exact acest lucru printr-un template cum am exemplificat. Template-ul ia ca parametru tipul care reprezintă magma și o operație binară reprezentată de o funcție compatibilă. În C++, putem folosi ca parametri de template valori constante, inclusiv funcții. Astfel, structura definită aici poate apela funcția dată ca parametru în template și să satisfacă conceptul IsMagma la compile-time.

template<typename TypeT, TypeT cross_operation(const TypeT&, const TypeT&)>
struct magma
{
const TypeT value;

explicit magma(const TypeT &value) : value(value) {}

constexpr magma cross(const magma &other) const
{
return magma(cross_operation(value, other.value));
}

// ...
}

Dacă vom folosi structura declarată mai sus într-un context constant, vom putea vedea că, într-adevăr, se satisface condiția de IsMagma. Astfel, acest exemplu:

int plus(const int &a, const int &b)
{
return a + b;
}

int main()
{
if constexpr (IsMagma<int>)
{
std:cout << "Type int is a magma\r\n";
}
else
{
std:cout << "Type int is not a magma\r\n";
}

if constexpr (IsMagma<magma<int, plus>>)
{
std:cout << "Type magma<int, plus> is a magma\r\n";
}
else
{
std:cout << "Type magma<int, plus> is not a magma\r\n";
}

return 0;
}

Va fi evaluat ca următorul cod:

int plus(const int &a, const int &b)
{
return a + b;
}

int main()
{
std:cout << "Type int is not a magma\r\n";
std:cout << "Type magma<int, plus> is a magma\r\n";

return 0;
}