This article describes how to throw away template parameter pack expansions in C++.
Motivation#
In template metaprogramming (TMP) in C++, you may want to discard pack expansion results after evaluating them. However, pack expansions are allowed in limited contexts. For example, the following code will not compile due to a syntax error:
/// Assign 0 to all of `arr[Indices...]`
template <int... Indices>
constexpr void Clear(Array& arr) noexcept {
arr[Indices] = 0...;
// ↑ error!
// You cannot expand parameter pack here.
}
Therefore, one needs a particular idiom to throw away expanded values in such a situation.
The following text describes how to discard evaluation results of pack expansions before C++17 and after C++17.
After C++17#
It is easier in C++17 than in C++14. One can throw away pack expansions by fold expressions using operator,()
. Fold expressions are a new syntax introduced in C++17, which can recursively apply binary operators to parameter packs. Typically, arithmetic or logical operators like +
and &&
are widely used, but operator,()
is also applicable.
template <int... Indices>
constexpr void Clear(Array& arr) noexcept {
((arr[Indices] = 0), ...);
}
Compilers interpret the above example as (arr[I0] = 0), ((arr[I1] = 0), ((arr[I2] = 0), ((...
. The expression executes all expressions from the starting one and does a pack expansion while throwing away results.
As described above, it is straightforward to discard pack expansion results in C++17.
Before C++17#
Before C++17, one can throw away pack expansion results only in the following situations:
- Function arguments
f(args...)
- Initializer lists
{args...}
I recommend the latter because in the former, the order of evaluation passed to a function is unspecified. Example:
/// (not recommended) A function that ignores all arguments
template <typename... Args>
constexpr void ConsumeValues(Args&&...) noexcept {}
template <int... Indices>
constexpr void Clear(Array& arr) noexcept {
ConsumeValues((arr[Indices] = 0)...);
// ↑ It is ok, but I don't recommend this code
// Because the order of argument evaluation is unspecified.
}
Therefore, the following code is better to throw away values before C++17. This idiom is sometimes called swallow idiom.
/// An empty struct that is construcrible by any value
struct Anything {
template <typename T>
constexpr Anything(T&&) noexcept {}
};
/// A empty function that takes an initializer list of `Anything`
/// As `Anything` is constructible by any type, you can discard values by
/// ```
/// ConsumeValues({/* 式1 */, /* 式2 */, ...});
/// ```
constexpr void ConsumeValues(std::initializer_list<Anything>) noexcept {}
First, define a struct Anything
that is constructible by any value. The type is implicitly convertible from any instance, so std::initializer_list<Anything>
acts as a trash box for any expression.
ConsumeValues({33, 4, "hoge", 44.5});
/// All values is converted `Anything`, and removed away
Using this trash box, you can discard the expansion results of parameter packs.
template <int... Indices>
constexpr void Clear(Array& arr) noexcept {
ConsumeValues({(arr[Indices] = 0)...});
}
Because arguments of initializer lists are evaluated from beginning to end, the swallow idiom is guaranteed to evaluate expansions from beginning to end.
template <int... Indices>
constexpr void Func(Array& arr) noexcept {
ConsumeValues({(Something(arr[Indices]),0)...});
}