为什么在初始化器列表可用时使用可变参数? [英] Why use variadic arguments now when initializer lists are avaiable?

查看:133
本文介绍了为什么在初始化器列表可用时使用可变参数?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在想,什么是可变参数优于初始化列表的优点。两者都提供相同的能力 - 向函数传递无限数量的参数。



我个人认为初始化列表有点更优雅。语法不那么尴尬。



此外,看起来初始化列表的性能随着参数数量的增加明显更好。



那么我缺少什么,除了在C中使用可变参数的可能性吗?

解决方案

参数你的意思是椭圆(如 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}); //错误!无法推导出T
}

因为这个原因,初始化列表不常用于类型扣除是必需的,除非参数的类型确实意味着是同质的。另一方面,Variadic模板提供了一个类型安全版本的省略号可变参数列表。



此外,调用一个函数一个初始化列表需要在一对大括号中包含参数,而函数采用一个可变参数包就不是这样。



最后(还有其他差异,但是这些是与您的问题更相关的),初始化器列表中的值是 const 对象。根据C ++ 11标准的第18.9 / 1段:


类型 initializer_list< E> / code>提供对 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& amp;)< std :: endl; }
};

void foo(std :: initializer_list< X> const& l){}

int main()
{
X x,y, z,w;
foo({x,y,z,std :: move(w)}); //将打印X(X const&)三次
//和X(X&&)一次
}

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

  template< typename ... Ts> 
void bar(Ts& ... args)
{
std :: cout< bar(Ts& ...)< std :: endl;
//可以在这里完成转发并将
//参数传递给另一个函数...
}

int main()
{
X x,y,z,w;
bar(x,y,z,std :: move(w)); //只打印bar(Ts& ...)
}






(*)但是,必须注意,初始化器列表(与C ++标准库的所有其他容器不同)具有引用语义 ,因此虽然元素的复制/移动在将元素插入初始化器列表时执行,复制初始化器列表本身不会导致包含对象的任何复制/移动(如上面引用的标准段落中所述):

  int main()
{
X x,y,z,w;
auto l1 = {x,y,z,std :: move(w)}; //将打印X(X const&)三次
//和X(X&&)一次

auto l2 = l1; //将不打印
}


I've been wondering what are the advantages of variadic arguments over initializer lists. Both offer the same ability - to pass indefinite number of arguments to a function.

What I personally think is initializer lists are a little more elegant. Syntax is less awkward.

Also, it appears that initializer lists have significantly better performance as the number of arguments grows.

So what am I missing, besides the possibility to use use variadic arguments in C as well?

解决方案

If by variadic arguments you mean the ellipses (as in void foo(...)), then those are made more or less obsolete by variadic templates rather than by initializer lists - there still could be some use cases for the ellipses when working with SFINAE to implement (for instance) type traits, or for C compatibility, but I will talk about ordinary use cases here.

Variadic templates, in fact, allow different types for the argument pack (in fact, any type), while the values of an initializer lists must be convertible to the underlying type of the initalizer list (and narrowing conversions are not allowed):

#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
}

Because of this, initializer lists are less often used when type deduction is required, unless the type of the arguments is indeed meant to be homogenous. Variadic templates, on the other hand, provide a type-safe version of the ellipses variadic argument list.

Also, invoking a function that takes an initializer list requires enclosing the arguments in a pair of braces, which is not the case for a function taking a variadic argument pack.

Finally (well, there are other differences, but these are the ones more relevant to your question), values in an initializer lists are const objects. Per Paragraph 18.9/1 of the C++11 Standard:

An object of type initializer_list<E> provides access to an array of objects of type const E. [...] Copying an initializer list does not copy the underlying elements. [...]

This means that although non-copyable types can be moved into an initializer lists, they cannot be moved out of it. This limitation may or may not meet a program's requirement, but generally makes initializer lists a limiting choice for holding non-copyable types.

More generally, anyway, when using an object as an element of an initializer list, we will either make a copy of it (if it is an lvalue) or move away from it (if it is an rvalue):

#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
}

In other words, initializer lists cannot be used to pass arguments by reference (*), let alone performing perfect forwarding:

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&&...)"
}


(*) It must be noted, however, that initializer lists (unlike all other containers of the C++ Standard Library) do have reference semantics, so although a copy/move of the elements is performed when inserting elements into an initializer list, copying the initializer list itself won't cause any copy/move of the contained objects (as mentioned in the paragraph of the Standard quoted above):

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
}

这篇关于为什么在初始化器列表可用时使用可变参数?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆