DEV Community

Mark Nefedov
Mark Nefedov

Posted on

C++20 Concepts

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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<>;
        };
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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; }
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
pauljlucas profile image
Paul J. Lucas

You don't need ValueType. All of the STL container classes already have value_type, e.g., typename Vec::value_type. Same for Size: typename Vec::size_type.

Collapse
 
denim06111991 profile image
denim06111991

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.