Skip to content

Concepts

Concepts specify constraints on template parameters, enabling better error messages and cleaner template code.

Concepts were introduced in C++20 to address long-standing issues with template metaprogramming. They allow you to specify what a template parameter must be capable of doing, rather than just accepting any type.

The standard library provides many useful concepts in the <concepts> header:

#include <concepts>
#include <type_traits>
// Common concepts
template<typename T> concept Integral = std::integral<T>;
template<typename T> concept FloatingPoint = std::floating_point<T>;
template<typename T> concept Signed = std::signed_integral<T>;
template<typename T> concept Unsigned = std::unsigned_integral<T>;
template<typename T> concept Copyable = std::copyable<T>;
template<typename T> concept Movable = std::movable<T>;
template<typename T> concept DefaultConstructible = std::default_initializable<T>;
template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; };
#include <concepts>
#include <iostream>
// Define a concept that checks if a type supports addition
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // Must support + operator
};
// Define a concept with return type constraint
template<typename T>
concept Numeric = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
{ a - b } -> std::convertible_to<T>;
{ a * b } -> std::convertible_to<T>;
{ a / b } -> std::convertible_to<T>;
};
// Using the concept
template<Addable T>
T add(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << std::endl; // 3
std::cout << add(1.5, 2.5) << std::endl; // 4.0
// add("hello", "world"); // Error: no matching operator+
}
#include <concepts>
#include <vector>
// Method 1: Template constraint
template<std::integral T>
T factorial(T n) {
if (n <= 1) return T(1);
return n * factorial(n - 1);
}
// Method 2: Short form with constrained auto
template<std::integral T>
T factorial(T n) {
T result = 1;
for (T i = 2; i <= n; i++) {
result *= i;
}
return result;
}
// Method 3: requires clause
template<typename T>
requires std::integral<T>
T factorial(T n) {
if (n <= 1) return T(1);
return n * factorial(n - 1);
}
#include <concepts>
#include <iostream>
// Constrained polymorphic variable
std::integral auto i = 42;
std::floating_point auto f = 3.14;
void process(std::integral auto value) {
std::cout << "Integer: " << value << std::endl;
}
void process(std::floating_point auto value) {
std::cout << "Float: " << value << std::endl;
}
int main() {
process(10); // Calls integer version
process(3.14); // Calls floating version
}
#include <concepts>
// And combination
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// More complex concept
template<typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
{ a == b } -> std::convertible_to<bool>;
};
// Using requires with multiple conditions
template<typename T>
concept Hashable = requires(T value) {
{ std::hash<T>{}(value) } -> std::convertible_to<std::size_t>;
} && requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>;
{ a != b } -> std::convertible_to<bool>;
};
#include <concepts>
#include <iostream>
// Concept with multiple parameters
template<typename T, typename U>
concept CommonArithmetic =
std::integral<T> && std::integral<U> ||
std::floating_point<T> && std::floating_point<U>;
// Or use requires
template<typename T, typename U>
requires std::integral<T> && std::integral<U>
auto add(T a, U b) {
return a + b;
}
int main() {
std::cout << add(5, 10) << std::endl; // 15
std::cout << add(5L, 10) << std::endl; // 15
}

The C++20 standard library includes many useful concepts:

#include <concepts>
#include <type_traits>
// Core language concepts
template<typename T> concept same_as<T, U>;
template<typename T, typename U> concept convertible_to<T, U>;
template<typename T> concept movable<T>;
template<typename T> concept copyable<T>;
template<typename T> concept semiregular<T>;
template<typename T> concept regular<T>;
// Comparison concepts
template<typename T> concept equality_comparable<T>;
template<typename T> concept totally_ordered<T>;
// Object concepts
template<typename T> concept default_initializable<T>;
template<typename T> concept move_constructible<T>;
template<typename T> concept copy_constructible<T>;
// Callable concepts
template<typename F, typename... Args> concept invocable<F, Args...>;
template<typename F, typename... Args> concept regular_invocable<F, Args...>;
  1. Better Error Messages: Instead of cryptic template errors, you get clear messages about what constraint was violated

  2. Self-Documenting Code: Concepts make template requirements explicit

  3. Faster Compilation: Compiler can reject invalid instantiations earlier

  4. IDE Support: Better IntelliSense and autocomplete

#include <concepts>
#include <iostream>
#include <vector>
#include <string>
// Custom concepts
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
template<typename T>
concept Printable = requires(std::ostream& os, T value) {
{ os << value } -> std::convertible_to<std::ostream&>;
};
// Function templates using concepts
template<Numeric T>
T square(T value) {
return value * value;
}
template<Addable T>
T sum(T a, T b) {
return a + b;
}
template<Printable T>
void print(const T& value) {
std::cout << value << std::endl;
}
int main() {
// Works with integers
std::cout << square(5) << std::endl; // 25
std::cout << sum(10, 20) << std::endl; // 30
// Works with doubles
std::cout << square(3.5) << std::endl; // 12.25
std::cout << sum(1.5, 2.5) << std::endl; // 4.0
// Works with strings
print("Hello, World!");
// This would fail at compile time:
// square("hello"); // Error: not Numeric
// sum(1, "world"); // Error: not Addable
return 0;
}
  1. Use standard library concepts when possible
  2. Keep concepts simple and focused
  3. Prefer requires expression syntax
  4. Document complex constraints
  5. Test concept constraints with static_assert
// Test concepts at compile time
static_assert(std::integral<int>);
static_assert(std::floating_point<double>);
static_assert(Numeric<int>);
static_assert(Addable<std::string>);
  • Concepts specify constraints on template parameters
  • Use requires expression to define custom concepts
  • Prefer standard library concepts when available
  • Concepts improve error messages and code readability
  • Constrained auto (C++20) simplifies template code
#include <concepts>
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<Numeric T>
T add(T a, T b) {
return a + b;
}
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // Must support + operator
};
template<Addable T>
T doubleValue(T value) {
return value + value;
}
template<typename T>
requires std::integral<T>
T factorial(T n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// Short form
template<std::integral T>
T factorial(T n) {}
// Constrained auto
std::integral auto x = 42;
  • Better error messages
  • Self-documenting templates
  • Faster compilation