6

为了扩展灵活的函数参数,有一种方法使用std::initializer_list. 然而我无法理解。任何人都可以以一种可以理解的方式解释这一点吗?

template<typename T, typename... Args>
auto print(T value, Args... args) {
    std::cout << value << std::endl;
    return std::initializer_list<T>{([&] {
        std::cout << args << std::endl;
    }(), value)...};
}
4

1 回答 1

5

这是一种非常混乱的做事方式,但 C++14 要求我们做类似的事情。我将解释这些限制以及为什么以这种方式完成(尽管有更清晰的方法来做到这一点)。

这段代码的目标是重复打印出它在单独的行上给出的每个参数。由于该函数是可变参数模板,因此需要对表达式使用包扩展std::cout << args << std::endl

你的第一个想法可能是(std::cout << args << std::endl) ...;。但是,这实际上并不是您可以在 C++14 中执行的有效操作。实际上,您只能在 C++14 中以逗号分隔的值序列的上下文中执行包扩展,例如函数的参数列表或其他。你不能只是将一个包扩展为一个赤裸裸的声明。

好吧,您可以将包扩展到的一个地方是一个花括号初始化列表({}用于初始化对象)。但是,{(std::cout << args << std::endl) ...};也不起作用。扩展没有错;问题是括号初始化列表本身。从语法上讲,braced-init-list 只能在初始化对象时出现。并且作为语句的赤裸裸{}不会初始化任何东西。所以你不能在那里使用它。

所以你必须使用{}来初始化一些东西。典型的用法是初始化一个空数组。例如:

int unused[] = {0, ((std::cout << args << std::endl), 0)...};

如果为空,0,则需要首字母;args您无法初始化没有元素的未调整大小的数组。扩展表达式中的尾随, 0是逗号表达式的一部分。

在 C++ 中,表达式的(1, 2)意思是“计算表达式 1,然后丢弃它的值,计算表达式 2,并将其用作总表达式的结果。” 所以在包扩展中使用它意味着`输出一个参数,丢弃结果,并使用 0 作为表达式的结果。因此,就表达式的结果而言,每个包扩展只是一种非常奇特的“0”表达方式。

最后,unused只存储一堆零。我们使用初始化的副作用unused来强制 C++ 解压缩包扩展。

在您展示的代码中,用户决定使用花括号初始化列表直接初始化一个initializer_list<T>. 这也是有效的,并且它具有处理空的args. 问题是用户然后返回这个对象。这很糟糕,因为没有人可以真正使用该返回值。

initializer_lists 不拥有它们引用的对象。在包含这些对象的支撑初始化列表的引用处创建一个临时数组;initializer_listjust 指向该数组。临时数组将在return语句结束时被销毁,因此调用者将获得initializer_list指向一堆被销毁对象的那个。T此外,它不必要地调用了一堆复制构造函数,因为没有人可以使用返回值。

所以这是一个常见习语的混乱和糟糕的例子。最好将其删除return并使其成为一个void功能。

C++17 只允许我们直接执行此操作,而无需初始化数组:

((std::cout << args << std::endl), ...);

这是对逗号运算符的折叠表达式。它将首先调用每个子表达式,直到args.

于 2020-09-14T14:25:25.383 回答