Constraining Template Arguments
How can I create a template that only accepts certain types of arguments?
Constraining template arguments is a powerful feature in C++ that allows you to restrict which types can be used with your templates. This can help prevent errors, improve compile-time error messages, and make your code more self-documenting.
There are several ways to achieve this, with the most modern and recommended approach being concepts, introduced in C++20.
Here are the main approaches, starting with the most modern:
1. Concepts (C++20 and later)
Concepts provide a way to specify constraints on template arguments directly in the template declaration.
#include <concepts>
#include <iostream>
// Define a concept
template <typename T>
concept Numeric = std::integral<T>
|| std::floating_point<T>;
// Use the concept in a template
template <Numeric T>
T Add(T a, T b) {
return a + b;
}
int main() {
// Works with int
std::cout << Add(5, 3) << '\n';
// Works with double
std::cout << Add(3.14, 2.86) << '\n';
// This would not compile
Add("Hello", "World");
}
error: 'Add': no matching overloaded function found
note: could be 'T Add(T,T)'
note: the associated constraints are not satisfied
2. SFINAE (Substitution Failure Is Not An Error)
For pre-C++20 code, SFINAE is a common technique to constrain templates.
#include <iostream>
#include <type_traits>
template <
typename T,
typename = std::enable_if_t<std::is_arithmetic_v<T>>
>
T Add(T a, T b) {
return a + b;
}
int main() {
// Works with int
std::cout << Add(5, 3) << '\n';
// Works with double
std::cout << Add(3.14, 2.86) << '\n';
// This would not compile
Add("Hello", "World");
}
error: 'Add': no matching overloaded function found
note: could be 'T Add(T,T)'
note: 'T Add(T,T)': could not deduce template argument for '<unnamed-symbol>'
3. Static Assertions
While not as flexible as the above methods, static assertions can be used to provide clear error messages when constraints are violated.
#include <iostream>
#include <type_traits>
template <typename T>
T Add(T a, T b) {
static_assert(
std::is_arithmetic_v<T>,
"Add only works with numeric types"
);
return a + b;
}
int main() {
// Works with int
std::cout << Add(5, 3) << '\n';
// Works with double
std::cout << Add(3.14, 2.86) << '\n';
// This would cause a static assertion failure
Add("Hello", "World");
}
error: static_assert failed: 'Add only works with numeric types'
4. Tag Dispatching
This technique uses function overloading and tag types to select different implementations based on type properties.
#include <iostream>
#include <type_traits>
// Implementation for arithmetic types
template <typename T>
T Add(T a, T b, std::true_type) {
return a + b;
}
// Implementation for non-arithmetic types
// (will cause a compile error)
template <typename T>
T Add(T a, T b, std::false_type) {
static_assert(
std::is_arithmetic_v<T>,
"Add only works with numeric types"
);
// Never reached due to the static_assert
return T{};
}
// Public interface
template <typename T>
T Add(T a, T b) {
return Add(a, b, std::is_arithmetic<T>{});
}
int main() {
// Works with int
std::cout << Add(5, 3) << '\n';
// Works with double
std::cout << Add(3.14, 2.86) << '\n';
// This would cause a compile error
Add("Hello", "World");
}
error: static_assert failed: 'Add only works with numeric types'
Each of these approaches has its strengths:
- Concepts provide the clearest syntax and best error messages.
- SFINAE is widely supported and can be very flexible.
- Static assertions provide very clear error messages but are less flexible.
- Tag dispatching can be used to select entirely different implementations based on type properties.
When constraining template arguments, choose the approach that best fits your needs and the C++ version you're targeting. In modern C++, concepts are generally the preferred approach when available.
Class Templates
Learn how templates can be used to create multiple classes from a single blueprint