Skip to content

Concepts and Requires — C++20 Constraints, std::integral, std::ranges::input_range, Template Constraints

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Concepts and Requires. We cover key concepts, practical examples, and best practices to help you master this topic.

C++20 concepts introduce named constraints on template parameters, enabling readable type requirements with clear error messages and simpler generic code compared to SFINAE-based enable_if patterns.

What You'll Learn

You will define your own concepts with the concept keyword and requires expressions, use standard library concepts like std::integral, std::floating_point, and std::ranges::input_range, overload functions based on concept satisfaction, combine concepts with logical operators, and replace SFINAE with concepts for cleaner template code.

Why It Matters

SFINAE and enable_if work but produce cryptic error messages dozens of lines long. Concepts solve this with constraints that the compiler checks and reports by name. When you see template <std::integral T>, the intent is obvious. Concepts also improve overload resolution, code completion in IDEs, and make C++ generic programming accessible to developers who found template Metaprogramming intimidating.

Learning Path

graph LR
    A["47: constexpr & consteval"] --> B["48: Concepts & Requires"]
    B --> C["49: Type Traits & Metaprogramming"]
    C --> D["50: Lambda Expressions"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Basic Concepts

A concept is a named boolean predicate evaluated at compile time on template arguments.

#include <iostream>
#include <concepts>

// Define a concept
template <typename T>
concept Integral = std::is_integral_v<T>;

// Use the concept to constrain a template
template <Integral T>
T half(T value) {
    return value / 2;
}

// Equivalent syntax with 'requires'
template <typename T>
    requires Integral<T>
T doubleIt(T value) {
    return value * 2;
}

int main() {
    std::cout << half(42) << "\n";      // 21 (int satisfies Integral)
    std::cout << doubleIt(7) << "\n";   // 14

    // half(3.14);  // Error: double does not satisfy Integral
    // Error message: constraints not satisfied
}

The compiler error when calling half(3.14) will explicitly say "constraints not satisfied" and show the concept Integral, making it immediately clear why the call failed.

Standard Library Concepts

C++20 provides over 50 standard concepts in <concepts> and <ranges>.

#include <iostream>
#include <concepts>
#include <string>

// std::integral, std::floating_point, std::signed_integral, std::unsigned_integral
// std::same_as, std::derived_from, std::convertible_to
// std::regular, std::semiregular, std::equality_comparable
// std::totally_ordered, std::movable, std::copyable
// std::invocable, std::predicate, std::relation

template <std::integral T>
T addOne(T value) {
    return value + 1;
}

template <std::floating_point T>
T addOne(T value) {
    return value + 1.0;
}

// Multiple constraints
template <typename T>
    requires std::integral<T> || std::floating_point<T>
auto addOneGeneric(T value) {
    return value + 1;
}

// Negated concept
template <typename T>
    requires !std::integral<T> && !std::floating_point<T>
auto addOneGeneric(T value) {
    return value + 1;  // May not compile for non-arithmetic types
}

int main() {
    std::cout << addOne(5) << "\n";       // 6 (integral)
    std::cout << addOne(3.14) << "\n";    // 4.14 (floating_point)
    // addOne(std::string("hello"));      // Error: no matching overload
}

Writing Custom Concepts

Use requires expressions to specify what a type must support.

#include <iostream>
#include <vector>
#include <list>
#include <forward_list>

// Concept: type has begin() and end()
template <typename T>
concept Range = requires(T& t) {
    std::begin(t);
    std::end(t);
};

// Concept: type has size()
template <typename T>
concept Sized = requires(const T& t) {
    { t.size() } -> std::convertible_to<size_t>;
};

// Combined concept
template <typename T>
concept SizedRange = Range<T> && Sized<T>;

// Function constrained by combined concept
template <SizedRange T>
void processSizedRange(const T& container) {
    std::cout << "Processing " << container.size() << " elements\n";
    for (const auto& elem : container) {
        std::cout << elem << " ";
    }
    std::cout << "\n";
}

// Simple requires clause (no named types)
template <typename T>
    requires requires(T a, T b) { a + b; }
auto sum(T a, T b) {
    return a + b;
}

int main() {
    std::vector<int> v = {1, 2, 3};
    processSizedRange(v);  // Processing 3 elements

    // std::forward_list<int> fl = {1, 2, 3};
    // processSizedRange(fl);  // Error: forward_list has no size()
}

Requires Expression Forms

Requires expressions can check four kinds of requirements:

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

// 1. Simple requirement: expression must be valid
template <typename T>
concept HasBegin = requires(T t) {
    t.begin();  // must compile
};

// 2. Type requirement: typename T::type must exist
template <typename T>
concept HasValueType = requires {
    typename T::value_type;  // type alias must exist
};

// 3. Compound requirement: expression + return type constraint
template <typename T>
concept Iterable = requires(T t) {
    { t.begin() } -> std::input_or_output_iterator;
    { t.end() } -> std::input_or_output_iterator;
};

// 4. Nested requirement: additional constraint
template <typename T>
concept ContiguousIterable = Iterable<T> && requires(T t) {
    { t.data() } -> std::contiguous_iterator;
};

// Full requires expression in one
template <typename T>
concept Container = requires(T t, const T ct, size_t i) {
    typename T::value_type;
    typename T::size_type;
    typename T::iterator;
    typename T::const_iterator;

    { t.begin() } -> std::same_as<typename T::iterator>;
    { ct.begin() } -> std::same_as<typename T::const_iterator>;
    { t.size() } -> std::same_as<typename T::size_type>;
    t.swap(t);
    // ...
};

int main() {
    std::cout << ContiguousIterable<std::vector<int>> << "\n";  // 1
    // std::cout << ContiguousIterable<std::list<int>> << "\n"; // 0
}

Concept Overloading

Concepts participate in overload resolution with a clear ordering.

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

template <typename T>
    requires std::integral<T>
void classify() {
    std::cout << "Integral type\n";
}

template <typename T>
    requires std::floating_point<T>
void classify() {
    std::cout << "Floating-point type\n";
}

// Catch-all for arithmetic
template <typename T>
    requires std::arithmetic<T>
void classify() {
    std::cout << "Arithmetic type\n";
}

// More constrained overload is preferred
template <typename T>
    requires std::integral<T> && (sizeof(T) == 1)
void classify() {
    std::cout << "Single-byte integral type\n";
}

int main() {
    classify<int>();                // Single-byte integral? No -> Integral type
    classify<char>();               // Single-byte integral type
    classify<double>();             // Floating-point type
    // classify<std::string>();     // Error: no matching function
}

When multiple constrained templates match, the compiler selects the one with the "most constrained" requirements (subsumption rules).

Concepts vs SFINAE

Concepts produce dramatically better error messages.

#include <iostream>
#include <type_traits>
#include <concepts>

// SFINAE version (C++11-17)
template <typename T>
std::enable_if_t<std::is_integral_v<T>, void>
process_sfinae(T value) {
    std::cout << "SFINAE: " << value << "\n";
}

// Concepts version (C++20)
template <std::integral T>
void process_concept(T value) {
    std::cout << "Concept: " << value << "\n";
}

int main() {
    process_sfinae(42);    // Works
    process_concept(42);   // Works

    // Uncomment to compare errors:
    // process_sfinae("hello");   // ~20 lines of cryptic error
    // process_concept("hello");  // ~3 lines: "constraints not satisfied"
}

Concepts also enable auto with constraints:

#include <iostream>
#include <concepts>

// Constrained auto parameters
void printNumber(std::integral auto value) {
    std::cout << "Number: " << value << "\n";
}

// Constrained lambda (C++20)
auto add = []<std::integral T>(T a, T b) {
    return a + b;
};

int main() {
    printNumber(42);  // OK
    // printNumber("hello");  // Error

    std::cout << add(3, 5) << "\n";  // 8
}

Real-World: Ranges Concepts

The Ranges library heavily uses concepts.

#include <iostream>
#include <ranges>
#include <vector>
#include <algorithm>

// Function constrained to work only with sorted, random-access ranges
template <std::ranges::random_access_range Rng>
    requires std::ranges::sized_range<Rng>
bool binarySearch(const Rng& range, const std::ranges::range_value_t<Rng>& value) {
    return std::ranges::binary_search(range, value);
}

int main() {
    std::vector<int> v = {1, 3, 5, 7, 9};
    std::cout << binarySearch(v, 5) << "\n";   // 1 (true)
    std::cout << binarySearch(v, 6) << "\n";   // 0 (false)
}

Common Mistakes

Mistake 1: Confusing concept definition with requires clause

template <typename T>
concept C1 = requires(T t) { t.foo(); };  // Definition + requires expression

template <C1 T>
void func(T t) {}  // Using concept

template <typename T>
    requires requires(T t) { t.foo(); }  // Requires clause with requires expression
void func2(T t) {}

The double requires is correct but confusing. requires(T t) { ... } is a requires expression; the first requires in requires requires is the requires clause keyword.

Mistake 2: Not understanding concept subsumption

template <std::integral T> void f(T);  // More constrained
template <std::integral T> requires (sizeof(T) > 1) void f(T);  // More constrained

Concepts subsume based on their definition, not arbitrary expressions. The sizeof version is more constrained only if the compiler can prove it.

Mistake 3: Using concepts only for error messages

Concepts also improve overload resolution, enable if constexpr checks with requires, and speed compilation by providing earlier failure.

Mistake 4: Over-constraining

template <typename T>
concept TooStrict = requires(T t) {
    { t.foo() } -> std::same_as<int>;  // Must return exactly int
    { t.foo() } -> std::convertible_to<int>;  // More flexible
};

Mistake 5: Forgetting that concepts are boolean predicates

template <typename T>
concept MyConcept = std::is_integral_v<T> && requires(T t) { ... };

Concepts must be constant expressions evaluable at compile time. They cannot depend on runtime values.

Practice Questions

  1. What is the output?
template <std::integral T> void f(T) { std::cout << "int"; }
void f(double) { std::cout << "double"; }

int main() { f(3.14); }

Answer: double — the non-template function is preferred over the constrained template.

  1. What does a requires expression check? Answer: It checks that certain expressions are valid, types exist, and return types satisfy constraints — all at compile time.

  2. Write a concept that checks if a type has a length() method returning size_t. Answer:

template <typename T>
concept HasLength = requires(const T& t) {
    { t.length() } -> std::same_as<size_t>;
};
  1. Can a function template have multiple concepts on different parameters? Answer: Yes: template <std::integral T, std::floating_point U> void func(T, U);.

  2. What happens when no constrained template matches? Answer: The compiler generates an error listing the constraints that were checked and how each failed, usually in a few concise lines.

FAQ

What are C++20 concepts

Concepts are named compile-time predicates that constrain template parameters. They specify requirements that types must satisfy, with clear error messages when requirements are not met.

How do concepts differ from SFINAE

Concepts are readable, reusable, produce clear error messages, support better overloading, and are checked earlier in the compilation process. They replace most SFINAE use cases.

What is a requires expression

A requires expression is a compile-time boolean that checks whether a set of expressions, types, or nested requirements are valid for given template arguments.

Can I combine concepts with logical operators

Yes. Concepts support &&, ||, and !. The compiler understands concept subsumption for ordering constrained overloads.

Are concepts checked before or after instantiation

Before. The compiler verifies constraints at the point of call, before template instantiation, producing earlier and clearer error messages.

Mini Project

Implement a generic accumulate function that works only with ranges whose value type supports addition:

#include <iostream>
#include <vector>
#include <list>
#include <string>
#include <concepts>

// Your accumulate with concepts

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    std::cout << accumulate(v) << "\n";  // 15

    std::list<double> lst = {1.5, 2.5, 3.0};
    std::cout << accumulate(lst) << "\n";  // 7.0

    std::vector<std::string> sv = {"hello", " ", "world"};
    // accumulate(sv);  // Works if string supports +
}

This project mirrors how real C++ libraries like std::ranges use concepts to Express type requirements clearly, and is directly comparable to Java interfaces as a constraint mechanism.

What's Next

You now use concepts to write readable, constrained templates. Next, you will explore type traits and template metaprogramming — techniques for computing types at compile time, building on everything from SFINAE to constexpr.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro