In recent years, C++ has evolved dramatically, bringing powerful abstractions and stricter compile‑time checks. One of the most significant additions in C++20 is the concepts feature. Concepts provide a way to express intent for template parameters, enabling more readable code, better diagnostics, and improved compilation times. In this article, we’ll dive into the fundamentals of concepts, illustrate their practical benefits, and walk through a real‑world example that showcases how they can transform a generic library.
1. What Are Concepts?
At its core, a concept is a compile‑time predicate that describes the requirements a type must satisfy. Think of it as a contract: a template can specify that its type argument must meet the “Iterator” concept, the “Movable” concept, or any user‑defined predicate. The compiler verifies that the supplied type satisfies the concept, and if not, it produces a clear diagnostic.
Unlike SFINAE or enable_if, concepts are declarative and integrated into the language syntax. This integration means that constraints are checked before template overload resolution, yielding more precise error messages and eliminating the need for many workarounds.
2. Defining a Simple Concept
A concept is declared with the concept keyword, followed by a name and a parameter list. Inside the body, you write an expression that must be valid for the types that satisfy the concept. The expression is evaluated in a concept context, where the parameters are assumed to be of the placeholder type.
template <typename T>
concept Incrementable = requires(T a) {
{ ++a } -> std::same_as<T&>; // pre‑increment returns T&
{ a++ } -> std::same_as <T>; // post‑increment returns T
};
In this example, any type T that supports both pre‑ and post‑increment operations (with the expected return types) satisfies the Incrementable concept.
3. Using Concepts in Function Templates
Once a concept is defined, you can constrain a function template by placing the concept before the template parameter list or in a requires clause.
template <Incrementable T>
T add_one(T value) {
return ++value;
}
The compiler now checks that any type passed to add_one satisfies Incrementable. If you attempt to call add_one with an int, it works; if you pass a std::string, the compiler produces a clear error indicating that std::string does not satisfy Incrementable.
4. Concepts and Overload Resolution
Concepts influence overload resolution directly. Consider two overloaded functions:
template <typename T>
void process(T) requires Incrementable <T> { /* ... */ }
template <typename T>
void process(T) requires std::integral <T> { /* ... */ }
When you call process with an int, the second overload is selected because int satisfies both concepts, but the compiler picks the more constrained overload. This behavior eliminates ambiguity and improves clarity.
5. A Real‑World Example: A Generic Queue
Let’s build a lightweight generic queue that operates only on movable types, using concepts to enforce the requirement.
#include <concepts>
#include <vector>
#include <iostream>
template <typename T>
concept Movable = std::movable <T>;
template <Movable T>
class SimpleQueue {
public:
void push(T&& value) {
storage_.push_back(std::move(value));
}
T pop() {
if (empty()) throw std::out_of_range("queue empty");
T value = std::move(storage_.back());
storage_.pop_back();
return value;
}
bool empty() const { return storage_.empty(); }
private:
std::vector <T> storage_;
};
The Movable concept ensures that only types that can be moved are allowed, preventing accidental use with non‑movable types (e.g., types containing std::mutex). Attempting to instantiate SimpleQueue<std::mutex> results in a compile‑time error, giving developers immediate feedback.
6. Benefits Over Traditional Techniques
| Feature | Traditional (SFINAE/enable_if) | Concepts |
|---|---|---|
| Readability | Templates are cluttered with std::enable_if_t<...>* = nullptr |
Clean, declarative constraints |
| Error Messages | Often cryptic, pointing to instantiation failures | Clear diagnostics indicating which requirement failed |
| Overload Resolution | Requires manual ordering of overloads | Compiler selects most constrained overload automatically |
| Maintainability | Constraints scattered, hard to modify | Centralized, reusable concept definitions |
7. Common Pitfalls and Tips
- Implicit vs. Explicit Requirements – Concepts only check expressions that are used in the body. If you need to guarantee that a type has a specific member, write that requirement explicitly.
- Namespace Pollution – Keep concepts in a dedicated namespace or header to avoid naming collisions.
- Combining Concepts – You can compose concepts using logical operators:
template <typename T> concept Arithmetic = std::integral<T> || std::floating_point<T>; - Performance – Concepts are compile‑time only; they impose no runtime overhead.
8. Conclusion
C++20 concepts provide a powerful, type‑safe way to articulate template requirements. By making constraints explicit, they improve code clarity, diagnostics, and maintainability. Whether you’re writing a generic container, a serialization library, or simply want to enforce stronger type contracts in your project, concepts are the modern tool you should embrace.
Happy coding—and may your templates always satisfy their concepts!