C++ 初始化列表展开,这个代码是什么意思?

C++ 初始化列表展开,这个代码是什么意思?

https://www.zhihu.com/question/443285720/answer/1723184923

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)...};
}

代码意图

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), ...);
}
posted @ 2021-02-21 15:55  py2020  阅读(299)  评论(0)    收藏  举报