当初始化列表可用时,为什么现在要使用可变参数呢?

Why use variadic arguments now when initializer lists are available?

本文关键字:变参 参数 为什么 列表 初始化 当初      更新时间:2023-10-16

我一直想知道可变参数相对于初始化列表有什么优势。两者都提供相同的功能——向函数传递不确定数量的参数。

我个人认为初始化列表更优雅一些。语法不那么别扭了。

而且,随着参数数量的增加,初始化列表的性能也明显提高。

那么,除了在C中使用use可变参数的可能性之外,我还错过了什么?

如果可变参数指的是省略号(如void foo(...)),那么这些省略号或多或少会被可变模板而不是初始化列表所淘汰——在使用SFINAE实现(例如)类型特征或C兼容性时,省略号仍然可能有一些用例,但我将在这里讨论普通用例。

可变模板实际上允许参数包的不同类型(实际上,任何类型),而初始化列表的值必须转换为初始化列表的基础类型(不允许窄化转换):

#include <utility>
template<typename... Ts>
void foo(Ts...) { }
template<typename T>
void bar(std::initializer_list<T>) { }
int main()
{
    foo("Hello World!", 3.14, 42); // OK
    bar({"Hello World!", 3.14, 42}); // ERROR! Cannot deduce T
}

因此,除非实参的类型确实是同质的,否则在需要类型演绎时很少使用初始化列表。另一方面,可变参数模板提供了一个类型安全的版本的省略号可变参数列表。

同样,调用接受初始化列表的函数需要将实参括在一对花括号中,而接受可变参数包的函数则不是这样。

最后(好吧,还有其他的区别,但这些与你的问题更相关),初始化列表中的值是const对象。c++ 11标准第18.9/1段:

类型为initializer_list<E>的对象提供了对类型为 const E 的对象数组的访问。[…复制初始化列表可以而不是复制底层元素。[…]

这意味着尽管不可复制类型可以移动到初始化列表中,但不能将其移出。这个限制可能满足也可能不满足程序的要求,但通常使初始化列表成为保存不可复制类型的限制选择。

更一般地说,无论如何,当使用对象作为初始化列表的元素时,我们要么复制它(如果它是左值),要么移开它(如果它是右值):

#include <utility>
#include <iostream>
struct X
{
    X() { }
    X(X const &x) { std::cout << "X(const&)" << std::endl; }
    X(X&&) { std::cout << "X(X&&)" << std::endl; }
};
void foo(std::initializer_list<X> const& l) { }
int main()
{
    X x, y, z, w;
    foo({x, y, z, std::move(w)}); // Will print "X(X const&)" three times
                                  // and "X(X&&)" once
}

换句话说,初始化列表不能用于通过引用(*)传递参数,更不用说执行完美的转发了:

template<typename... Ts>
void bar(Ts&&... args)
{
    std::cout << "bar(Ts&&...)" << std::endl;
    // Possibly do perfect forwarding here and pass the
    // arguments to another function...
}
int main()
{
    X x, y, z, w;
    bar(x, y, z, std::move(w)); // Will only print "bar(Ts&&...)"
}

然而,必须注意的是,初始化列表(与c++标准库的所有其他容器不同)确实具有引用语义,因此,尽管在向初始化列表插入元素时执行元素的复制/移动,但复制初始化列表本身不会导致所包含对象的任何复制/移动(如上面引用的标准段所述):
int main()
{
    X x, y, z, w;
    auto l1 = {x, y, z, std::move(w)}; // Will print "X(X const&)" three times
                                       // and "X(X&&)" once
    auto l2 = l1; // Will print nothing
}

简单地说,C风格的可变变量函数在编译时产生的代码比c++风格的可变变量模板少,所以如果你担心二进制大小或指令缓存压力,你应该考虑用可变变量来实现你的功能,而不是作为模板。

然而,可变变量模板明显更安全,并且产生更多可用的错误消息,因此您通常会想要用内联可变变量模板包装行外的可变变量函数,并让用户调用该模板。