Reputation: 22123
What I mean is, for example, a constructor for a class like the following:
class vector<size_t N, typename FLOAT=double> {
vector(FLOAT ...x) {} // I want exactly N arguments here
};
I hope it's clear that I do not want a variadic function, but a function that takes exactly N arguments, when N is known at compile-time. Thus, using the example above, vector<3>(1.5, 2.5)
should produce a compile-time error, while vector<2>(1.5, 2.5)
should compile and run.
Is this possible?
I was thinking that perhaps this could be done with parameter packs, but I'm not quite sure how.
Upvotes: 11
Views: 1115
Reputation: 1695
Adapted from @jarod42’s answer.
#include <utility> // index sequence stuff
/// Has a member type `type` that is `T`. Every other template parameter is ignored.
template<typename T, typename U, U>
struct value_constant_type { using type = T; };
/// Helper alias.
template<typename T, typename U, U u>
using value_constant_type_t = typename value_constant_type <T, U, u>::type;
/// Primary template (won’t be defined)
template<…, std::size_t n, …, typename = std::make_index_sequence<n>>
class C; // won’t be defined
/// Partial specialization that infers `Is...` of length `n`.
template<…, std::size_t n, …, std::size_t... Is>
class C<…, n, …, std::index_sequence<Is...>>
{
using X = …; // X is any type you want.
public:
/// Takes exactly `n` arguments of type `X`
C(value_constant_type_t<X, std::size_t, Is>... args)
{ … }
};
It works like this: If you don’t pass a type for the last (unnamed) template type parameter of C
, which is what happens for every reasonable instantiation, the default argument gets used: std::make_index_sequence<n>
.
Then, the compiler must check if any partial/full template specialization is matched, and in that case, the partial template specialization dubbed “actual implementation” will be matched. However, to match it, the compiler must infer all the arguments (which is trivial for the ones you repeat), but in particular, it matches Is...
, so those are available in the specialization.
In the constructor (it doesn’t need to be a constructor, it could be any parameter list), value_constant_type_t<X, std::size_t, Is>... args
expands the value list Is
and repeats value_constant_type_t<X, std::size_t, i>
for every i
in the value list Is
. The utility value_constant_type_t
ignores the arguments std::size_t
and i
and “evaluates” to X
; that, in total, gives you n
copies of X
. Calling the constructor with any number of arguments other than n
fails, and with n
arguments, every one of them is bound to a X
parameter. The constructor is not a template.
There are little restrictions on X
. It can be a Y&
, const Y&
, or Y&&
, or whatever else you like.
What won’t work is making the constructor a template and inferring X
from arguments. If it’s not a constructor, but a function, you could pass a type argument for X
explicitly. That is because in value_constant_type <T, U, u>::type
, T
is on the left-hand side of an ::
, which means it’s never being inferred through this.
If you want inference, you have to be a little more creative and compromise to some degree: You need two specializations then: One for the n = 0
case, where there won’t be any arguments, and one for the non-zero case, where there is a distinguished first argument from which we can infer T
.
template<std::size_t n, typename = std::make_index_sequence<n>>
class C;
template<>
class C<0, std::index_sequence<>>
{
public:
C()
{
std::cout << "0-arg case" << std::endl;
}
};
template<std::size_t n, std::size_t... Is>
class C<n, std::index_sequence<0, Is...>>
{
public:
template<typename T>
C(T arg, value_constant_type_t<T, std::size_t, Is>... args)
{
std::cout << (1 + sizeof...(Is)) << "-arg case" << std::endl;
}
};
(Play with this code on godbolt.org.) Here, we define two specializations: A full specialization (if you have more template parameters, it’ll be a partial specialization) and a partial one. The full specialization handles the n = 0
case with an empty index sequence. The partial specialization matches the 0
entry of the sequence “manually,” so that it won’t actually be part of Is...
, i.e. Is...
matches indices 1
… n-1
and will in fact be empty for n = 1
. (If, for some reason, you need the values of the index sequence starting from 0
, just use (Is - 1)
where you would use Is
. For throwing them into value_constant_type_t
, their values don’t matter, though.
Note: As of writing this answer, the question was tagged c++, but with no particular version. AFAICT, excluding macros, this can’t be done without variadic templates, so this applies to C++11 and onwards.
For C++20, I would recommend an approach like this:
template<std::size_t n>
class C
{
public:
template<typename... Ts>
requires (sizeof...(Ts) == n)
C(Ts... args)
requires (std::convertible_to<Ts, std::common_type_t<Ts...>> && ...)
{ … }
};
The error messages are a little clearer (how much depends on the compiler) and the implementation is a lot easier to read. This is the reason for the two separate requires
clauses: If you use the wrong number of arguments, some compilers (GCC, Clang) tell you which constraint isn’t satisfied, but e.g. MSVC unfortunately doesn’t (as of 2024-08-01).
Upvotes: 0
Reputation: 2655
Probably the simplest way is just to use static_assert
. Add pattern matching as appropriate:
template<int N, typename... Args>
void foo(Args... args) {
static_assert(sizeof...(args) == N, "Incorrect number of arguments");
// stuff
}
This will do most of the time, even allowing you to have a nice friendly custom error.
You can even go a bit crazy and use foldexprs to apply even more constraints on the arguments:
static_assert(((std::is_integral_v<Args>) && ...), "All arguments must be integers");
The value of this approach compared to SFINAE-based solutions is that you can get less insane compile errors.
If using C++20, you can use the new requires
keyword to apply the same concept in a more powerful/friendly way.
Upvotes: 1
Reputation: 217283
With some indirection, you may do something like:
template <std::size_t, typename T> using alwaysT = T;
template <typename FLOAT, typename Seq> struct vector_impl;
template <typename FLOAT, std::size_t... Is>
struct vector_impl<FLOAT, std::index_sequence<Is...>> {
vector_impl(alwaysT<Is, FLOAT>... floats) { /*...*/}
};
template <std::size_t N, typename FLOAT>
using vector = vector_impl<FLOAT, std::make_index_sequence<N>>;
Upvotes: 12