C++ 初始化列表展开,这个代码是什么意思?
代码意图
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}
printf3 函数的意图是将参数打印出来,一行一个。比如
printf3(1, 2, 3, "HelloWorld");
的输出结果为
1
2
3
HelloWorld
printf3第二行有点难懂,它同时结合了 C++ 的几个语法点
- 初始化列表 initializer_list
- 可变参数模版,Variadic Template
- lambda 表达式
- 逗号表达式
初始化列表
初始化列表,还是比较好懂的,比如下面代码
auto tmp = {1, 2, 3, 4};
for (auto& v : tmp) {
printf("%d\n", v);
}
tmp 的类型就是 std::initializer_list<int>。初始化列表的值不能包含不同类型,下面代码会编译错误
auto tmp = {"Hello", 1, 2, 3}; // error
也不能是 void值,下面代码也会编译错误
auto tmp = {void, void, void}; // error
可变参数模版
可变参数模版,就是模版中 typename... Ts ,和 Ts... args 这种语法。
template<typename T, typename... Ts>
auto printf3(T value, Ts... args)
可以将多个输入参数打包(Parameter Pack)到 args 中。比如 printf3(1, 2, 3, 4);,args 就是 2、3、4 三个参数 pack 到 args 中。在模板代码中,可以通过 .... 的语法将参数展开(Parameter Pack Expansion) ,比如下面代码
template <typename... Ts>
void doSomething(Ts... args) {
auto tmp = {args...};
for (auto &v : tmp) {
printf("%d\n", v);
}
}
调用 doSomething(1, 2, 3);时,tmp 会展开成
auto tmp = {1, 2, 3};
args...这种只是简单用法。模板参数还可以在更复杂的表达式中展开,比如
auto tmp = { (args * args)...};
auto tmp1 = { std::sin(args)...};
auto tmp2 = { (std::sin(args) + args)...};
调用 doSomething(1, 2, 3);时,tmp 会展开成
auto tmp = {(1 * 1),(2 * 2),(3 * 3)};
auto tmp1 = { std::sin(1), std::sin(2), std::sin(3) };
auto tmp2 = { (std::sin(1) + 1), (std::sin(2) + 2), (std::sin(2) + 2) };
C++ 碰到 ...,就会将包含 args 的表达式展开,可以在很复杂的表达式中展开。
lambda 表达式
下面的 fn 就是先定义了 lambda 表达式,之后再进行调用
int a = 0;
auto fn = [&a] {
std::cout << a << std::endl;
};
fn();
也可以将 lambda 表达式定义和调用写在一起。比如
int a = 0;
auto b = [&a] {
return a;
}();
逗号表达式
这个也比较简单。C++ 中,假如表达式用逗号分隔,会以此求值,最后一个值就是表达式的值。比如
auto a = (funA(), funB(), funC());
会以此求值 funA()、funB()、funC(),最终 a 为 funC 的值。
拆解原始代码
对几个语法点简单了解后,我们现在来拆解
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
首先,里面的复杂表达式
([&args] {
std::cout << args << std::endl;
}(), value)...
会按照模板参数依次展开,求值后,生成一个std::initializer_list<T>。
比如调用为 printf3(1, 2, "HelloWorld"); value 值为 1,args 为 2、"HelloWorld" 两个参数中的打包。将包含 args 的表达式展开,展开的代码大致为
void printf3(int value, int arg0, const char *arg1) {
std::cout << value << std::endl;
(void)std::initializer_list<int>{
([&arg0] {
std::cout << arg0 << std::endl;
}(), value),
([&arg1] {
std::cout << arg1 << std::endl;
}(), value)
};
}
arg0 和 arg1 都分别嵌在逗号表达式中,于是分别调用了 lambda 函数,并 value 作为逗号表达式的值。于是 std::initializer_list放的都是 int 类型。
各个模板参数展开,依次调用了 lambda 函数,打印出各自的值。
一些细节
- 代码中的
(void)std::initializer_list,这个 (void) 是为了防止编译警告,因为 initializer_list 没有被使用。 - 先定义了
T value,再定义Ts... args,是让printf3最少包含一个参数,让printf3()调用报编译错误。 - 使用逗号表达式,是因为代码中,那个 lambda 函数返回值为 void, 而
std::initializer_list<void>是不合法的。这里也可以让 lambda 返回一个值,省去逗号表达式。比如
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<int>{([&args] {
std::cout << args << std::endl;
return 1;
}())...};
}
- 原始代码搞得比较复杂,也为了让
printf3中的参数可以有不同的类型。比如下面的简单实现,就只能传入相同类型参数。
template <typename T, typename... Ts>
auto printf3(T value, Ts... args) {
auto tmp = {value, args...};
for (auto &v : tmp) {
std::cout << v << std::endl;
}
}
- C++17 中有个 Pack fold expression 的语法,可以让实现更简单些
template <typename T, typename... Ts>
void printf3(T value, Ts... args) {
std::cout << value << std::endl;
((std::cout << args << std::endl), ...);
}

浙公网安备 33010602011771号