To demonstrate the expressive power of the C++ language we will take a trivial example from algebra, a magma.
A magma is defined as a tuple where is a set and is a binary operation .
In C++20, this mathematical structure translates very simply and elegantly as:
#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;
}
};
}
Concepts
To explain the code above, we first need to understand what concepts are in C++. The concepts officially appeared in C++20, although they existed as technical specifications before their appearance in the standard.
A concept is a compile-time predicate, which means that they are evaluated in any constant context. This means that they can be used as constraints on template parameters and can be evaluated in constant expressions, such as in constexpr if.
A concept can evaluate a property of generic parameters. For example, in the standard library there is the concept std::same_as which checks whether two types given as parameters are the same or not. For example, the following code:
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";
}
It will be evaluated as: `
std::cout << "Types int and int are the same\r\n";
std::cout << "Types int and std::string are not the same\r\n";
One can already see the advantage of concepts in C++ over the usual use of macros. But we go one step further on the example given above. The HasCrossOperation concept defined here tests a data type if it meets a condition, the condition being that the type has a method named cross that represents the binary operation in magma and for any two values of the given type this method returns the same type.
template<typename TypeT>
concept HasCrossOperation =
requires (const TypeT &value_a, const TypeT &value_b)
{
{ value_a.cross(value_b) } -> std::same_as<TypeT>;
};
Any concept can be evaluated using other concepts. The IsMagma concept will evaluate a type if it is a magma. Because structs and classes in C++ can have methods, we will condition only that the type satisfies the HasCrossOperation condition, although we can use other concepts with the logical operators && and ||. In our example, if a data type is itself a set of values and if it has an associated method as a binary operation, we already have a magma.
template<typename TypeT>
concept IsMagma = HasCrossOperation<TypeT>;
Alternatively, if we want to call that operation infixed and not infixed, we can also declare a function with the said constraint.
template<IsMagma TypeT>
constexpr TypeT call_cross(const TypeT &value_a, const TypeT &value_b)
{
return value_a.cross(value_b);
}
Here we have created a generic function to restrict the generic type to only those that satisfy the magma condition.
However, we want to fully capture the concept of magma which, by definition, is a tuple. We can express exactly this through a template as we have exemplified. The template takes as parameter the type representing magma and a binary operation represented by a compatible function. In C++, we can use constant values, including functions, as template parameters. Thus, the structure defined here can call the function given as a parameter in the template and satisfy the IsMagma concept at 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));
}
// ...
}
If we use the structure declared above in a constant context, we can see that the IsMagma condition is indeed satisfied. Thus, this example:
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;
}
It will be evaluated as the following code:
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;
}