Part 1
You have a very interesting question, thanks! I was always thinking same way - why standard C++ has no way of iterating typed pack Ts && ... args
through regular dynamical loop.
Usually people write huge and ugly templated functions for each case of usage to iterate and expand arguments pack.
So inspired by your question I decided to implement just now a handy helper function that will allow dynamical access to elements of arguments pack inside regular dynamical loop.
At the bottom of my answer there is a full code that implements my idea. My helper function is called dget(idx, lambda, args...)
, wich means "dynamic get". Provided index idx
and pack of arguments args...
it applies lambda
to argument at position idx
.
My function dget()
is impelemnted very efficiently through switch (idx)
. In most cases it will have zero overhead for getting element of pack if you compile with -O2
or -O3
option. It will be optimized away to same code as regular templated helper functions that is usually written for expansion of arguments pack.
In case if enough optimization is not possible, for example if loop can't be unrolled and/or if you get through using non-optimizable run time index (e.g. volatile) then still my code will be very efficient, because it is well known that all compilers optimize switch (idx)
(in case of integer idx
) as regular jump-table indexed by integer, so accessing idx-th element will have overhead of 1 or 2 runtime instructions to do actual jump.
One advanced example of usage of my dget()
is inside to_str()
function located in full code at the end of my answer, here below I provide central loop of this function, to give some explanations:
std::string s;
for (size_t i = 0; i < sizeof...(args); ++i)
s += dget(i, [i](auto const & x){
if constexpr(IS_TYPE(x, std::string))
return std::to_string(i) +
":str: '" + std::string(x) + "'";
else if constexpr(IS_TYPE(x, int))
return std::to_string(i) +
":int: " + std::to_string(x);
else if constexpr(IS_TYPE(x, bool))
return std::to_string(i) +
":bool: " + std::string(x ? "true" : "false");
else {
static_assert([]{ return false; }());
return std::to_string(i) + std::string(":<bad_type>");
}
}, args...) + ", ";
this to_str()
is just a Toy example, it might be implemented more efficiently and shorter and withoug dget()
with using just std::stringstream
, but I made it more complex just to show how to use specifically dget()
.
Basically this loop above naturally in run time iterates through all elements of provided args...
pack and depending on type of argument converts it to string (with prefix) in a way specific for each type. You can see that I have regular run-time access to i
-th element of args pack by using Visitor pattern with lambda.
One may argue that example block of code above can be implemented without loop, as following:
std::string s;
([&](auto const & x){
if constexpr(IS_TYPE(x, std::string))
s += "str: '" + std::string(x) + "', ";
else if constexpr(IS_TYPE(x, int))
s += "int: " + std::to_string(x) + ", ";
else if constexpr(IS_TYPE(x, bool))
s += "bool: " + std::string(x ? "true" : "false") + ", ";
else {
static_assert([]{ return false; }());
s += std::string("<bad_type>") + ", ";
}
}(args), ...);
but in this case you have two drawbacks: 1) my dget()
can return a value that can be naturally used in natural loop logic, like I used in first code snippet. 2) in the last example we don't have i
variable any more so we can't access or know index of the element, it may be a blocker for using non-loop variant. Anyway loop to me looks much more natural.
My dget()
is very similar to std::visit that is used to access std::variant. Also I could implement my dget()
through variant+visit, but I didn't want to have any overhead that has conversion to dynamical variant and back again. Of course good compiler will probably optimize away conversion to std::variant and back, but yet I decided to use switch (idx)
for my implementation. Although std::variant conversion is not a bad choice as well. Maybe later I update my answer to include Part 2 where I do same stuff using std::variant instead of switch, and measure theirs performance difference.
Your original printall()
function can also implemented easily and naturally through loop using my dget()
:
template <typename ... Ts>
void print_all(Ts && ... args) {
for (size_t i = 0; i < sizeof...(args); ++i)
dget(i, [](auto const & x){
std::cout << std::boolalpha << x << " ";
}, args...);
}
although solution of @GuillaumeRacicot is shorter and more canonical and better to use for this specific case of std::cout
.
So full code below. Click Try it online!
to see my code in action on Online servers. Also don't forget to see console Output
located after the code. Also you may try to come back to my answer later in case if I decide to extend it by writing Part 2 where I plan to use std::variant
for implementing same dget()
.
Try it online!
#include <cstdint>
#include <tuple>
#include <stdexcept>
#include <iostream>
#include <iomanip>
using std::size_t;
template <typename F, typename ... Args>
inline decltype(auto) dget(size_t idx, F && f, Args && ... args) {
#define C(i) \
case (i): { \
if constexpr(i < std::tuple_size_v<std::tuple<Args...>>) \
return f(std::get<i>(std::tie(std::forward<Args>(args)...))); \
else goto out_of_range; \
}
static_assert(sizeof...(Args) <= 30);
switch (idx) {
C( 0) C( 1) C( 2) C( 3) C( 4) C( 5) C( 6) C( 7) C( 8) C( 9)
C(10) C(11) C(12) C(13) C(14) C(15) C(16) C(17) C(18) C(19)
C(20) C(21) C(22) C(23) C(24) C(25) C(26) C(27) C(28) C(29)
default:
goto out_of_range;
}
#undef C
out_of_range:
throw std::runtime_error("dget: out of range!");
}
template <typename ... Ts>
void print_all(Ts && ... args) {
for (size_t i = 0; i < sizeof...(args); ++i)
dget(i, [](auto const & x){
std::cout << std::boolalpha << x << " ";
}, args...);
std::cout << std::endl;
}
template <typename ... Ts>
std::string to_str(Ts && ... args) {
#define IS_TYPE(x, t) (std::is_same_v<std::decay_t<decltype(x)>, t>)
std::string s;
for (size_t i = 0; i < sizeof...(args); ++i)
s += dget(i, [i](auto const & x){
if constexpr(IS_TYPE(x, std::string))
return std::to_string(i) +
":str: '" + std::string(x) + "'";
else if constexpr(IS_TYPE(x, int))
return std::to_string(i) +
":int: " + std::to_string(x);
else if constexpr(IS_TYPE(x, bool))
return std::to_string(i) +
":bool: " + std::string(x ? "true" : "false");
else {
static_assert([]{ return false; }());
return std::to_string(i) + std::string(":<bad_type>");
}
}, args...) + ", ";
#undef IS_TYPE
return s;
}
int main() {
print_all(123, "abc", true);
std::cout << to_str(123, std::string("abc"), true) << std::endl;
}
Output:
123 abc true
0:int: 123, 1:str: 'abc', 2:bool: true,
Part 2
Just out of curiosity I decided to implement second variant of dget()
implementation, described in Part 1.
This variant uses std::array table prefilled with std::function objects providing access to elements of args pack.
Initially I was planning to implement Part 2 of my answer using std::variant
, but figured out that variant is not simplifying anything, while having more overhead. Hence I decided to use std::array+std::function table.
This Part 2 implementation of dget()
may or may not be slower than Part 1 implementation, depending on how compiler optimizes the code. I didn't do any speed measurement, maybe in future I'll do them and update my answer. Small Part 2 overhead may be due to static
guard of table, which needs every time to check if static constant was already initialized, although it will always generate successful CPU's branch prediction. Also Part 2 might be slower due to usage of std::function which may wrap initial lambdas into virtual pointer dereferenced call.
Without extra explanation providing full code below, it has only other body of dget()
compared to code of Part 1. Test examples and console output is same.
Try it online!
#include <cstdint>
#include <tuple>
#include <stdexcept>
#include <iostream>
#include <iomanip>
#include <variant>
#include <functional>
#include <array>
using std::size_t;
template <typename F, typename ... Args>
inline decltype(auto) dget(size_t idx, F && f, Args && ... args) {
using RetT = decltype(f(std::get<0>(std::tie(std::forward<Args>(args)...))));
using FfT = decltype(std::forward<F>(f));
#define C(i) \
[](FfT f, decltype(std::forward<Args>(args)) ... argsi) { \
if constexpr(i < sizeof...(Args)) \
return f(std::get<i>(std::tie(std::forward<Args>(argsi)...))); \
else { \
throw std::runtime_error( \
"dget: programming logic, table access out of range"); \
return f(std::get<0>(std::tie(std::forward<Args>(argsi)...))); \
} \
},
static std::array<std::function<RetT(
FfT, decltype(std::forward<Args>(args))...
)>, 30> const tab = {
C( 0) C( 1) C( 2) C( 3) C( 4) C( 5) C( 6) C( 7) C( 8) C( 9)
C(10) C(11) C(12) C(13) C(14) C(15) C(16) C(17) C(18) C(19)
C(20) C(21) C(22) C(23) C(24) C(25) C(26) C(27) C(28) C(29)
};
#undef C
static_assert(sizeof...(Args) <= std::tuple_size_v<decltype(tab)>);
if (idx >= sizeof...(Args))
throw std::runtime_error("dget: index out of range!");
return tab[idx](std::forward<F>(f), std::forward<Args>(args)...);
}
template <typename ... Ts>
void print_all(Ts && ... args) {
for (size_t i = 0; i < sizeof...(args); ++i)
dget(i, [](auto const & x){
std::cout << std::boolalpha << x << " ";
}, args...);
std::cout << std::endl;
}
template <typename ... Ts>
std::string to_str(Ts && ... args) {
#define IS_TYPE(x, t) (std::is_same_v<std::decay_t<decltype(x)>, t>)
std::string s;
for (size_t i = 0; i < sizeof...(args); ++i)
s += dget(i, [i](auto const & x){
if constexpr(IS_TYPE(x, std::string))
return std::to_string(i) +
":str: '" + std::string(x) + "'";
else if constexpr(IS_TYPE(x, int))
return std::to_string(i) +
":int: " + std::to_string(x);
else if constexpr(IS_TYPE(x, bool))
return std::to_string(i) +
":bool: " + std::string(x ? "true" : "false");
else {
static_assert([]{ return false; }());
return std::to_string(i) + std::string(":<bad_type>");
}
}, args...) + ", ";
#undef IS_TYPE
return s;
}
int main() {
print_all(123, "abc", true);
std::cout << to_str(123, std::string("abc"), true) << std::endl;
}