Here is the clamp() function template from the previous Nibble:
#include <iostream>
template <typename T>
T clamp(T value, T lower, T upper){
if (value < lower) {
return lower;
}
if (value > upper) {
return upper;
}
return value;
}
int main(){
std::cout << clamp(120, 0, 100) << '\n';
std::cout << clamp(-19.2, 0.2, 1.0) << '\n';
}
// Terminal
100
0.2
Strangely, this doesn’t compile.
std::cout << clamp(10, 0.0, 100.0) << '\n';
/tmp/KiB8DdDtNB/main.cpp:21:23: error: no matching function for call to 'clamp(int, double, double)'
21 | std::cout << clamp(10, 0.0, 100.0) << '\n';
| ~~~~~^~~~~~~~~~~~~~~~
/tmp/KiB8DdDtNB/main.cpp:4:3: note: candidate: 'template<class T> T clamp(T, T, T)'
4 | T clamp(T value, T lower, T upper)
| ^~~~~
/tmp/KiB8DdDtNB/main.cpp:4:3: note: template argument deduction/substitution failed:
/tmp/KiB8DdDtNB/main.cpp:21:23: note: deduced conflicting types for parameter 'T' ('int' and 'double')
21 | std::cout << clamp(10, 0.0, 100.0) << '\n';
The compiler error gives us a clue: template argument deduction/substitution failed
During template argument deduction, the compiler tries to deduce one type for T. The template says all three parameters must have the same type:
T clamp(T value, T lower, T upper)
The compiler cannot deduce both int and double for the same T, so the call fails.
Since the arguments are 2 unique types, the compiler cannot perform a function template match.
Fortunately, there are 3 solutions to remedy this issue.
First solution – Use static_cast
Force the arguments to match by getting the caller to use static_cast.
std::cout << clamp(static_cast<double>(10), 0.0, 100.0) << '\n';
This works however it is awkward and difficult to read. Also, you don’t want to put the burden on the caller. The function should be user-friendly.
2nd solution – Specify the template argument
Specify the template argument. This creates a function max<double>(double, double). Due to implicit conversions, the int gets converted to a double.
std::cout << clamp<double>(10, 0.0, 100.0) << '\n';
Whilst this is better, the caller still has to remember the type. Plus, we wanted to use template argument deduction. This is a step backward.
Fortunately, we can do better.
3rd solution: Function templates with multiple template type parameters
We can give each argument its own template type parameter:
template <typename T, typename U, typename V>
auto clamp(T value, U lower, V upper){
if (value < lower) {
return lower;
}
if (value > upper) {
return upper;
}
return value;
}
Now the compiler can deduce different types:
T = int
U = double
V = double
This solves the deduction problem for the parameters.
However, there is an issue.
With auto return type deduction, all return statements in the function must deduce to a single consistent return type. In this template, one branch returns U, another returns V, and another returns T.
For some combinations, that can fail or produce behaviour that is not as explicit as we want.
Better solution: use std::common_type_t
The standard library provides std::common_type_t, which determines a common type that several types can be converted to.
For a mixed call such as:
clamp(10, 0.0, 100.0);
a reasonable common type is double.
Here is a stronger implementation:
#include <iostream>
#include <type_traits>
template <typename T, typename U, typename V>
std::common_type_t<T, U, V> clamp(T value, U lower, V upper){
using Common = std::common_type_t<T, U, V>;
Common v { static_cast<Common>(value) };
Common lo { static_cast<Common>(lower) };
Common hi { static_cast<Common>(upper) };
if (v < lo) {
return lo;
}
if (v > hi) {
return hi;
}
return v;
}
int main(){
std::cout << clamp(10, 0.0, 100.0) << '\n';
std::cout << clamp(-19.2, 0, 100) << '\n';
std::cout << clamp(120, 0, 100) << '\n';
}
Output:
10
0
100
This version accepts mixed numeric arguments and returns the common type.
The conversion strategy is explicit:
using Common = std::common_type_t<T, U, V>;
Then each input is converted to that common type before comparison.
That makes the function easier to reason about. The function is not merely accepting anything; it is saying:
Abbreviated function templates C++20
C++20 introduced abbreviated function templates.
Instead of writing:
template <typename T, typename U, typename V>
auto clamp(T value, U lower, V upper){
// ...
}
you can write:
auto clamp(auto value, auto lower, auto upper){
// ...
}
This is shorthand for a function template with separate invented template parameters for each auto parameter.
Conceptually, it behaves like:
template <typename T, typename U, typename V>
auto clamp(T value, U lower, V upper);
In conclusion
If CTAD is used with multiple argument types, CTAD fails. This is because a single template parameter T must deduce to one type.
This Nibble presented 3 solutions to make function templates compatible with multiple types.