Concepts (C++20)

Named type constraints for clearer template interfaces

The Template Problem

Traditional templates produce cryptic error messages when type requirements aren't met.

// C++17: Template with SFINAE
template<typename T>
auto square(T value) -> std::enable_if_t<std::is_arithmetic_v<T>, T> {
    return value * value;
}

// Calling with wrong type:
square(std::string{"hello"});  
// Error: no matching function... enable_if_t failed...
// User must understand SFINAE to interpret error

C++20: Concepts

Concepts are named sets of type requirements that produce clear error messages.

// C++20: Define a concept
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

// Use concept to constrain template
template<Arithmetic T>
T square(T value) {
    return value * value;
}

// Error is now clear:
// "constraint 'Arithmetic' was not satisfied"
// "'std::is_arithmetic_v<std::string>' evaluated to false"

Defining Concepts

1. Basic Syntax

// Simple concept using type trait
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Concept with expression requirements
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
};

// Concept with multiple requirements
template<typename T>
concept Container = requires(T t) {
    typename T::value_type;
    { t.begin() } -> std::same_as<typename T::iterator>;
    { t.end() } -> std::same_as<typename T::iterator>;
    { t.size() } -> std::convertible_to<std::size_t>;
};

2. Compound Concepts

// Combine concepts
template<typename T>
concept Sortable = Container<T> && 
    std::random_access_iterator<typename T::iterator> &&
    std::totally_ordered<typename T::value_type>;

// Refine existing concepts
template<typename T>
concept SignedIntegral = std::integral<T> && std::is_signed_v<T>;

template<typename T>
concept UnsignedIntegral = std::integral<T> && std::is_unsigned_v<T>;

Applying Concepts

1. Template Parameters

// Constrain template parameter
template<std::floating_point T>
T compute_area(T radius) {
    return 3.14159 * radius * radius;
}

// Multiple concepts
template<std::integral Index, typename Value>
Value get(const std::vector<Value>& v, Index i) {
    return v[i];
}

2. Abbreviated Function Templates

// Auto parameter with concept (C++20 abbreviated syntax)
void process(std::integral auto value) {
    // Only accepts integral types
}

// Multiple parameters
double divide(std::floating_point auto a, std::floating_point auto b) {
    return a / b;
}

3. Trailing Requires

// Requires clause
template<typename T>
    requires Addable<T> && std::copyable<T>
T sum(const std::vector<T>& values) {
    T result{};
    for (const auto& v : values) {
        result = result + v;
    }
    return result;
}

Standard Library Concepts

CategoryConcepts
Core Languagesame_as, derived_from, convertible_to
Arithmeticintegral, signed_integral, unsigned_integral, floating_point
Comparisonequality_comparable, totally_ordered
Objectcopyable, movable, default_initializable
Callableinvocable, regular_invocable, predicate
Iteratorinput_iterator, forward_iterator, random_access_iterator
Rangerange, sized_range, view, input_range

Real-World Example: Safe Math

#include <concepts>
#include <stdexcept>

template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Checked arithmetic - prevents overflow
template<Numeric T>
T safe_add(T a, T b) {
    if constexpr (std::integral<T>) {
        if (b > 0 && a > std::numeric_limits<T>::max() - b) {
            throw std::overflow_error("Integer overflow");
        }
        if (b < 0 && a < std::numeric_limits<T>::min() - b) {
            throw std::overflow_error("Integer underflow");
        }
    }
    return a + b;
}

// Vector operations
template<Numeric T>
class Vector2D {
    T x_, y_;
public:
    Vector2D(T x, T y) : x_(x), y_(y) {}
    
    T magnitude() const 
        requires std::floating_point<T>
    {
        return std::sqrt(x_ * x_ + y_ * y_);
    }
    
    auto dot(const Vector2D& other) const {
        return safe_add(safe_add(T{}, x_ * other.x_), y_ * other.y_);
    }
};

Concept Overloading

Concepts enable function overloading based on type capabilities:

// Fast path for random access iterators
template<std::random_access_iterator Iter>
void advance(Iter& it, int n) {
    it += n;  // O(1)
}

// Slower path for forward iterators
template<std::forward_iterator Iter>
void advance(Iter& it, int n) {
    for (int i = 0; i < n; ++i) {
        ++it;  // O(n)
    }
}

// Most general path
template<typename Iter>
void advance(Iter& it, int n) {
    // Fallback implementation
}

Best Practices

  • Name concepts after the operations they enable (Addable, Sortable, Drawable)
  • Use standard concepts when they exist (don't reinvent integral)
  • Prefer abbreviated syntax for simple cases
  • Don't over-constrain (e.g., requiring random_access when forward is enough)
  • Avoid concepts that combine unrelated requirements