第 1 部分
您有一个非常有趣的问题,谢谢!我一直在想同样的方式 - 为什么标准 C++ 无法通过常规动态循环迭代类型包 Ts && ... args。
通常人们为每种使用情况编写巨大而丑陋的模板函数来迭代和扩展参数包。
受您的问题启发,我决定现在实现一个方便的辅助函数,该函数将允许动态访问常规动态循环内的参数包元素。
在我的答案的底部有一个完整的代码来实现我的想法。我的辅助函数叫做dget(idx, lambda, args...),意思是“动态获取”。提供索引idx 和参数包args... 它将lambda 应用于位置idx 的参数。
我的函数dget() 通过switch (idx) 非常有效地执行。在大多数情况下,如果您使用-O2 或-O3 选项进行编译,则获取包元素的开销为零。它将被优化为与通常为扩展参数包而编写的常规模板化帮助函数相同的代码。
如果无法进行足够的优化,例如,如果无法展开循环和/或如果您通过使用不可优化的运行时索引(例如 volatile),那么我的代码仍然会非常高效,因为它众所周知,所有编译器都将switch (idx)(在整数idx 的情况下)优化为按整数索引的常规跳转表,因此访问第 idx 个元素将有 1 或 2 条运行时指令的开销来执行实际跳转。
使用我的dget() 的一个高级示例在我的答案末尾的完整代码中的to_str() 函数内部,下面我提供此函数的中心循环,以给出一些解释:
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...) + ", ";
这个to_str() 只是一个Toy 示例,它可能会更有效、更短且不使用dget(),只使用std::stringstream,但我让它变得更复杂只是为了展示如何专门使用dget()。
基本上,上面的这个循环在运行时自然地遍历提供的args... 包的所有元素,并根据参数的类型以特定于每种类型的方式将其转换为字符串(带前缀)。您可以看到,通过使用带有 lambda 的访问者模式,我可以定期运行时访问 args 包的 i-th 元素。
有人可能会争辩说,上面的示例代码块可以在没有循环的情况下实现,如下所示:
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), ...);
但是在这种情况下你有两个缺点:1)我的dget() 可以返回一个可以自然地用于自然循环逻辑的值,就像我在第一个代码 sn-p 中使用的一样。 2) 在最后一个示例中,我们不再有 i 变量,因此我们无法访问或知道元素的索引,它可能是使用非循环变体的障碍。无论如何,循环对我来说看起来更自然。
我的dget() 与用于访问std::variant 的std::visit 非常相似。我也可以通过变体+访问来实现我的dget(),但我不想有任何开销来转换为动态变体并再次返回。当然,好的编译器可能会优化转换为 std::variant 并返回,但我决定使用switch (idx) 来实现我的实现。虽然 std::variant 转换也不是一个坏选择。也许稍后我会更新我的答案以包括 第 2 部分,在其中我使用 std::variant 而不是 switch 做同样的事情,并测量他们的性能差异。
您原来的printall() 函数也可以使用我的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...);
}
尽管@GuillaumeRacicot 的solution 更短、更规范,更适合用于std::cout 的这种特定情况。
下面是完整的代码。单击Try it online! 以查看我在在线服务器上运行的代码。也不要忘记查看位于代码后面的控制台Output。此外,如果我决定通过编写 第 2 部分来扩展它,您也可以稍后再回复我的答案,我打算在其中使用 std::variant 来实现相同的 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;
}
输出:
123 abc true
0:int: 123, 1:str: 'abc', 2:bool: true,
第 2 部分
出于好奇,我决定实现dget() 实现的第二个变体,在第 1 部分中进行了描述。
此变体使用std::array 表预填充std::function 对象,提供对args 包元素的访问。
最初我计划使用std::variant 来实现我的答案的第 2 部分,但发现该变体并没有简化任何事情,而且开销更大。因此我决定使用 std::array+std::function 表。
dget() 的 第 2 部分 实现可能会也可能不会比 第 1 部分 实现慢,具体取决于编译器如何优化代码。我没有做任何速度测量,也许将来我会做它们并更新我的答案。小部分 2 的开销可能是由于表的static 保护,它需要每次检查静态常量是否已经初始化,尽管它总是会生成成功的 CPU 的分支预测。此外,由于使用了 std::function,第 2 部分可能会更慢,这可能会将初始 lambdas 包装到虚拟指针取消引用调用中。
没有额外的解释提供下面的完整代码,与第1部分的代码相比,它只有dget()的其他主体。测试示例和控制台输出是一样的。
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;
}