Skip to Content
Course content

60: Concepts and Constraints (C++20)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've spent any time writing templates in C++, you know the pain of the "template error wall of death." You pass the wrong type into a function, and the compiler gives you 200 lines of internal header traces before finally telling you that a type doesn't support the + operator. C++20 Concepts are essentially our way of telling the compiler: "Don't even try to instantiate this unless the type meets these specific requirements."

Why bother with Concepts when we have SFINAE?

Look, SFINAE (Substitution Failure Is Not An Error) worked for years, but let's be honest: std::enable_if_t is an eyesore. It's a hack. You end up burying your logic in return types or dummy template parameters, and it makes the function signature almost impossible to read at a glance. I've spent hours debugging enable_if logic just to get a simple overload to work.

Concepts turn those "hacks" into first-class language features. Instead of tricking the compiler into ignoring a function, you're explicitly stating the requirements. The biggest win isn't even the syntax—it's the error messages. When a Concept isn't met, the compiler tells you exactly which requirement failed, rather than dumping the entire template instantiation stack on your head.

How do I actually write and apply a custom Concept?

You use the concept keyword. Think of a concept as a named boolean predicate that the compiler evaluates at compile-time. You usually define it using a requires expression.

Let's say we're building a physics engine and we need a type that isn't just any number, but specifically something that can be added and multiplied. We can define a Numeric concept like this:

#include <concepts>
#include <iostream>

template<typename T>
concept Numeric = std::is_arithmetic_v<T> && requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
    { a * b } -> std::convertible_to<T>;
};

// Now we apply it. No more 'typename T'. Just 'Numeric T'.
template<Numeric T>
T calculate_force(T mass, T acceleration) {
    return mass * acceleration;
}

int main() {
    std::cout << calculate_force(10.5, 9.8) << "\n"; // Works great
    // calculate_force("heavy", 9.8); // This would fail immediately with a clear error
}

Notice the { a + b } -> std::convertible_to<T> part. That's a compound requirement. It doesn't just check if the + operator exists; it checks if the result of that operation can be converted back into the type T. It's a very powerful way to enforce interface contracts.

Can I use Concepts to overload functions based on type properties?

Yes, and this is where it gets really useful. Before C++20, if you wanted a different implementation of a function for integers versus floating-point numbers, you had to use messy template specializations. Now, you can just overload the function using different concepts.

I often use this when I want a "fast path" for simple types and a "robust path" for complex ones. Check this out:

#include <concepts>
#include <iostream>
#include <vector>

template<typename T>
concept Iterable = requires(T t) {
    { t.begin() } -> std::input_iterator;
    { t.end() } -> std::input_iterator;
};

// Overload 1: For single numeric values
void process_data(std::integral auto value) {
    std::cout << "Processing an integer: " <lt; value <lt; "\n";
}

// Overload 2: For floating point values
void process_data(std::floating_point auto value) {
    std::cout << "Processing a float: " <lt; value <lt; "\n";
}

// Overload 3: For things we can loop over
void process_data(Iterable auto container) {
    std::cout << "Processing a collection of size: " <lt; container.size() <lt; "\n";
}

int main() {
    process_data(42);              // Calls integral version
    process_data(3.14);            // Calls floating_point version
    process_data(std::vector{1, 2, 3}); // Calls Iterable version
}

Using auto in the function parameter list combined with a concept is called an "Abbreviated Function Template." It's significantly cleaner than the old template<typename T> void func(T val) syntax and makes the code feel much more like a modern language.




📋 Practical Task

Exercise: Implementing a Constrained `find_max` for Comparable Types

Your goal is to create a generic find_max function that finds the largest element in a collection. However, to prevent this function from being used with types that can't actually be compared (like a custom User struct without an operator>), you must implement a custom Concept.

Requirements:

  • Define a concept named Comparable. It should ensure that the type T supports the > operator and that the result of that operation is convertible to bool.
  • Implement a function find_max that takes a std::vector<T>. Use the Comparable concept to constrain the template parameter T.
  • Ensure the function handles empty vectors gracefully (e.g., by throwing an exception or returning a default).
  • Test the function with a std::vector<int> and a std::vector<std::string> to verify it works for different comparable types.
  • Try calling it with a vector of a custom struct Point { int x, y; }; (which has no comparison operator) to verify that the compiler now gives you a clear "concept not satisfied" error instead of a deep template trace.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.