I've tested variadic templates inside a function, which I developed step by step.
template <typename... Ts>
void foo (Ts...) noexcept {}
// Test code
int i {123};
int j {456};
foo(1,2,3,i,j);
It compiles.
Then I added a forwarding reference.
template <typename... Ts>
void foo (Ts&&...) noexcept {}
// Test code
int i {123};
int j {456};
foo(1,2,3,i,j);
This still compiles. At last, I decided to add a concept to contrain the function to work only with numbers.
template <typename T>
concept MyNumber =
(is_integral_v<T> || is_floating_point_v<T>);
template <MyNumber... Ts>
void foo (Ts&&...) noexcept {}
// Test code
int i {123};
int j {456};
foo(1,2,3,i,j); // does not compile
foo(1,2,3); // compiles
It does NOT compile. The solution would be to remove &&
in foo
's signature, or i
and j
to pass with std::forward
.
What is here the best practice? The usage of foo
should be easy as possible.
The problem with forwarding references T&&
is that
int
(e.g. i
) to the function,
T
deduces to int&
, andT&&
collapses to int&
(see reference collapsing)int
(e.g. 0
) to the function,
T
deduces to int
, andT&&
is simply int&&
In other words, the Ts
in your variadic function template are sometimes references int&
, and sometimes the type int
itself. is_integral_v<T>
is not true
when T
is a reference because e.g. int&
is not an integral type, it is a reference.
template <MyNumber... Ts>
void foo(Ts...) noexcept {}
// or with abbreviated function templates:
void foo(MyNumber auto...) noexcept {}
This is perfectly fine and actually better than using forwarding references because MyNumber
is only satisfied by small fundamental types like int
and float
that you should better pass by value anyway.
Personally, I think this solution is more elegant then the alternative:
template <typename... Ts>
requires ((MyNumber<std::remove_cvref_t<Ts>> && ...))
void foo(Ts&&...) noexcept {}
This would remove the &
from int&
when you're passing foo(i)
for the purpose of checking whether it satisfies MyNumber
.
MyNumber
conceptIdeally, concepts should be built from other concepts so that some concepts can be made more constrained than others:
#include <concepts>
template <typename T>
concept MyNumber = std::integral<T> || std::floating_point<T>;
Only use type traits like std::is_integral_v
when there is no equivalent concept in the standard library.