C++20 finally introduced to us SFINAE replacement – concepts. Concepts are a great example of compile-time polymorphism, they allow us easily to write generic code that will not spit to us cryptic errors.
For example, let’s write a function that will calculate the length of an arbitrary vector.
double norm(const std::vector<double>& vector){
double result = 0;
for (double i : vector)
result += std::pow(i, 2);
return std::sqrt(result);
}
And now we want to use this function with a vector with a non-standard allocator, with special structs for small vectors that stored on the stack. To describe all of these cases we can introduce a concept of “vector with floating-point values”, and we can use any Indexable container as vector storage, and we need to know the size of our vector.
First of all, we need to extract value type from Vector so it can be checked if it is a floating-point type, we can achieve this using std::decay.
template<typename Vec>
using ValueType = typename std::decay<decltype(Vec()[0])>::type;
We also need to make sure, that our vector can report it’s size.
template<typename Vector>
concept FloatVec =
std::is_floating_point_v<ValueType<Vector>> &&
requires (Vector vec) {
{ vec.size() } -> std::integral<>;
};
Now the only thing left is to update norm function. We need to deduce size type and make sure we are using the same value type for storing the result as we are using in vector.
template<FloatVec Vec>
auto norm(const Vec& vector) -> ValueType<Vec>{
using Size = decltype(vector.size());
ValueType<Vec> result = 0;
for (Size i = 0; i < vector.size(); ++i)
result += std::pow(vector[i], 2);
return std::sqrt(result);
}
Type traits also come in handy as an easy way to limit the usage of our templates. In this Vec2 we allowing only numbers to be used.
template<typename T> requires std::is_arithmetic_v<T>
struct Vec2{
float x,y;
size_t size() const { return 2; }
float operator[](size_t i) const{ return i == 0 ? x : y; }
};
Now we can use norm function with any vector as long as it implements FloatVec concept.
int main() {
std::vector<double> vector {1, 2, 2};
Vec2<float> vec2F {.x = 3, .y = 4};
std::byte stackStorage[64];
std::pmr::monotonic_buffer_resource memoryResource {stackStorage, sizeof(stackStorage)};
std::pmr::vector<double> pmrVector{{1,2,3,4}, &memoryResource};
std::cout <<
"std::vector: " << norm(vector) << // 3
"\nVec2f: " << norm(vec2F) << // 5
"\npmrVector: " << norm(pmrVector) << // 5.47723
std::endl;
return 0;
}
Top comments (2)
You don't need
ValueType
. All of the STL container classes already havevalue_type
, e.g.,typename Vec::value_type
. Same forSize
:typename Vec::size_type
.Quite a well written article @marknefedov
We are in the process of updating some books under our C++ portfolio. Over the next few weeks I am conducting a research in the C++ space to discover the top issues that C++ developers/programmers/experts like yourself are dealing with. We are reaching out to users to understand their needs/expectations and what they are looking for in C++ space.
After reviewing your profile, I believe that you could help me to gain a thorough understanding of what is working and not working for C++ developers like you.
Would you be happy to join me for a 10-minute call to discuss on how we can improvise our C++ books? Your inputs will be critical for us to bring forward a top-notch product for the C++ enthusiasts.