I'm trying to find the best way to pass a compile-time list of values to a utility function, as an exercise based on a real use case. This list will be subjected to some series of operations and the resulting value will be used in another operation in runtime. Below are some solutions I found, simplified to MWEs.
Of course, the operations are much more complex in the real use case, hence the need for these utility functions.
Solution 1: Parameter pack
template <int number>
constexpr int sum() {
return number;
}
template <int number, int next, int... rest>
constexpr int sum() {
return number sum<next, rest...>();
}
//API:
template <int... numbers>
inline void add(int& target) {
target = sum<numbers...>();
}
...
int number = 0;
add<1, 2, 3, 4, 5>(number);
Pros:
- Clean API
- Needs only c 14
Cons:
- Clunky implementation with recursion, painful to design and read when operations are complex
Solution 2: std::array
template <size_t N, std::array<int, N> numbers>
constexpr int sum() {
int ret = 0;
for (int number : numbers)
ret = number;
return ret;
}
//API:
template <size_t N, std::array<int, N> numbers>
inline void add(int& target) {
target = sum<N, numbers>();
}
...
int number = 0;
add<5, std::array{1, 2, 3, 4, 5}>(number);
Pros:
- Clean and readable implementation, easy to design no matter the complexity of operations
Cons:
- Super clunky API, size of list must be specified separately
- Needs c 20 to be able to pass inline
std::array
as non-type template parameter
Solution 3: std::array
wrapper
template <size_t N>
struct IntArray {
constexpr IntArray(std::array<int, N> arr_) : arr(arr_) {}
const std::array<int, N> arr;
};
template <IntArray numbers>
constexpr int sum() {
int ret = 0;
for (int number : numbers.arr)
ret = number;
return ret;
}
//API:
template <IntArray numbers>
inline void add(int& target) {
target = sum<numbers>();
}
...
int target = 0;
add<IntArray<5>({1, 2, 3, 4, 5})>(target);
Pros:
- Clean and readable implementation, easy to design no matter the complexity of operations
Cons:
- (Arguably) less but still clunky API, size of list must be specified separately
- Needs c 20 to be able to pass inline
IntArray
as non-type template parameter, and also to be able to omit theIntArray
template parameter value in at least the function definitions
Solution 4: std::initializer_list
template <std::initializer_list<int> numbers>
constexpr int sum() {
int ret = 0;
for (int number : numbers)
ret = number;
return ret;
}
template <std::initializer_list<int> numbers>
inline void add(int& target) {
target = sum<numbers>();
}
...
int target = 0;
add<{1, 2, 3, 4, 5}>(target);
Pros:
- Clean and readable implementation, easy to design no matter the complexity of operations
- Clean, usable and readable API
Cons:
- Doesn't actually compile (g 10.3.0 with
gnu 2a
):‘std::initializer_list<int>’ is not a valid type for a template non-type parameter because it is not structural
I have no idea what "not structural" means to be very honest. I'm actually surprised and disappointed by the fact that this approach doesn't work, given that std::initializer_list
is apparently fully constexpr
and std::array
works in the same situation. There seems to be a bug in the standard about the literalness of std::initializer_list
though: https://stackoverflow.com/a/28115954/1525238 In any case, I do see this as a missed opportunity for some really cool compile-time wizardry.
The question:
Can you suggest any way to improve the solutions above in any way possible, or suggest other solutions? Ideally the "best" solution would combine all of API and implementation cleanliness and readability, while requiring as low c standard as possible.
CodePudding user response:
Thanks to HolyBlackCat for suggesting to initialize a std::array
from the pack, which I didn't know was possible:
template <int... numbers>
constexpr int sum() {
std::array<int, sizeof...(numbers)> arr = {numbers...};
int ret = 0;
for(int number : arr)
ret = number;
return ret;
}
//API:
template <int... numbers>
inline void add(int& target) {
target = sum<numbers...>();
}
...
int number = 0;
add<1, 2, 3, 4, 5>(number);
This makes the implementation much cleaner. Requires c 17.
CodePudding user response:
Why don't you just take an initializer_list argument?
constexpr int sum(std::initializer_list<int> numbers) {
int ret = 0;
for (int number : numbers)
ret = number;
return ret;
}
int target = sum({1, 2, 3, 4, 5});