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:
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
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>'
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'
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:
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.
Answers to questions are automatically generated and may not have been reviewed.
Learn how templates can be used to create multiple classes from a single blueprint