Black Magic使用Initializer_list和包扩展

问题描述

要扩展灵活的功能参数,有一种使用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)...};
}

解决方法

这是一种非常混乱的处理方式,但是C ++ 14要求我们做类似的事情。我将解释其局限性以及为什么这样做(尽管有更清晰的方法)。

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

您的第一个想法可能是(std::cout << args << std::endl) ...;。但是,实际上这不是在C ++ 14中可以执行的有效操作。实际上,您只能在C ++ 14中以逗号分隔的值序列(例如,函数的自变量列表)中执行包扩展。您不能只是将包装扩展为裸露的语句。

好吧, 可以将包扩展到的一个地方是braced-init-list(用于初始化对象的{})。但是,{(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,并将其用作总表达式的结果。”因此在pack扩展中使用它意味着`输出一个参数,丢弃结果,并使用0作为表达式的结果。因此,就表达式的结果而言,每次扩展都只是说“ 0”的一种真正的幻想。

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

在您显示的代码中,用户决定使用braced-init-list直接初始化initializer_list<T>。这也是有效的,并且具有处理空args的次要好处。问题在于用户然后返回了该对象。这很糟糕,因为没有人可以实际使用该返回值。

initializer_list不拥有它们引用的对象。在支撑这些对象的braced-init-list引用处创建了一个临时数组。 initializer_list只是指向该数组。临时数组将在return语句的末尾销毁,因此调用者将获得一个initializer_list,该指针指向一堆 destroyed 对象。另外,由于没有人可以使用返回值,因此它会不必要地调用T的副本构造函数。

因此,这是一个常见成语的令人困惑和糟糕的例子。最好删除return并将其设置为void函数。

C ++ 17允许我们直接执行此操作而不必初始化数组:

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

这是fold expression上的逗号运算符。它将首先调用args中的值的每个子表达式。