RAMajd's daily notes

Notes about challenges I face during my day to day work

C++20 Concepts: Simple Constraints for Clean Code

In day-to-day C++ development with templates, missing constraints often leads to complex compile errors.

Concepts, introduced in C++20, solve this problem by providing a simple way to set requirements on template parameters at compile time.

cpp-concepts.png

What is a Concept?

A concept is a named set of compile-time requirements that a type must satisfy.

Instead of passing any type to a template and getting long compiler errors when a operation fails deep inside, concepts allow checking the type upfront.

Problem vs. Solution

Old Approach (C++11/C++14/C++17)

Without concepts, type errors only trigger when compiling the template body:

template <typename T>
T calculateAverage(T a, T b) {
    return (a + b) / 2;
}

int main() {
    // Fails deep inside function body with long error trace
    // calculateAverage(std::string("A"), std::string("B"));
}

Modern Approach (C++20 Concepts)

With concepts, we can explicitly define requirements using the concept keyword and requires clause:

#include <iostream>
#include <concepts>

// Custom concept definition
template <typename T>
concept Number = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
    { a / 2 } -> std::same_as<T>;
};

// Apply concept directly to template parameter
template <Number T>
T calculateAverage(T a, T b) {
    return (a + b) / 2;
}

int main() {
    std::cout << calculateAverage(10, 20) << std::endl; // OK: 15

    // Fails at call site with short error message
    // calculateAverage(std::string("A"), std::string("B"));
    return 0;
}

Syntax Options for Applying Concepts

C++20 provides several ways to apply concepts to template functions:

#include <concepts>

// 1. Concept as type parameter
template <std::integral T>
void process1(T val) {}

// 2. Trailing 'requires' clause
template <typename T>
requires std::integral<T>
void process2(T val) {}

// 3. Constrained auto syntax
void process3(std::integral auto val) {}

// 4. Combining multiple constraints
template <typename T>
requires std::integral<T> && (sizeof(T) >= 4)
void process4(T val) {}

Standard Library Concepts (<concepts>)

The standard library provides pre-defined concepts in the <concepts> header:

Concept Description
std::integral<T> Integer types (int, long, etc.)
std::floating_point<T> Floating-point types (float, double)
std::same_as<T, U> Exact type equivalence
std::derived_from<T, Base> Class inheritance relationship
std::movable<T> Move semantics support

Function Overloading Example

Concepts also simplify template function overloading:

#include <iostream>
#include <concepts>

void printValue(std::integral auto val) {
    std::cout << "Integer value: " << val << '\n';
}

void printValue(std::floating_point auto val) {
    std::cout << "Float value: " << val << '\n';
}

int main() {
    printValue(42);   // Integer overload
    printValue(3.14); // Float overload
    return 0;
}

Requirement Types in Custom Concepts

Inside concept definitions, four different types of requirements can be specified:

#include <concepts>

template <typename T>
concept PrintableAndSizeable = requires(T x) {
    // 1. Simple requirement: valid expression
    x.size();

    // 2. Type requirement: nested type exists
    typename T::value_type;

    // 3. Compound requirement: expression and return type check
    { std::cout << x } -> std::same_as<std::ostream&>;

    // 4. Nested requirement: additional logical constraint
    requires sizeof(T) > 8;
};

Summary

  • Better Error Messages: Errors point directly to the function call site.
  • Faster Compilation: Fails early during template resolution before instantiating templates.
  • Cleaner Code: Replaces long std::enable_if declarations with readable constraints.