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.
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_ifdeclarations with readable constraints.