传递初始化列表或容器,考虑使用移动语义?

11

编辑:在我们开始之前,这个问题不涉及std::initializer_list的正确使用方法;而是关于在需要方便的语法时应传递什么内容。感谢您保持主题。


C++11引入了std::initializer_list用于定义接受花括号初始化列表参数的函数。

struct bar {
    bar( char const * );
    bar( int );
} dog( 42 );

fn foo( std::initializer_list< bar > args );

foo( { "blah", 3, dog } );

这种语法很不错,但底层实现由于多种问题而不尽人意:

  • 它们不能被有意义地移动。上面的函数必须从列表中复制dog;这不能转换为移动构造或省略。只能使用移动语义的类型完全无法使用。(嗯,const_cast实际上是一个有效的解决方法。如果有关于这样做的文章,我想看看。)

  • 也没有constexpr语义。(在C++1y中即将推出。虽然这只是一个小问题。)

  • const不能像其他地方一样传播; initializer_list从来不是const,但其内容总是如此。(因为它不拥有其内容,所以它不能给予副本写入访问权限,尽管将其复制到任何位置都很少安全。)

  • initializer_list对象不拥有其存储空间(哎呀); 它与提供存储空间的完全独立的裸数组(哎呀)之间的关系定义得模糊不清(哎呀),就像引用与绑定临时变量的关系一样(四重哎呀)。

我相信这些问题将在适当的时候得到解决,但现在有没有一种最佳实践可以在不硬编码到initializer_list中的情况下获得优点?是否有任何关于绕过对其直接依赖的文献或分析?

明显的解决方案是传递一个标准容器(例如std::vector)的值。一旦从initializer_list中复制对象到它中,它就被移动构造为按值传递,然后你就可以移动内容了。改进方法是在栈上提供存储空间。一个好的库可能能够在不使用前者的情况下提供大部分initializer_listarrayvector的优势。

有什么资源可参考吗?


4
初始化列表称为“初始化列表”,是用于初始化对象。如果您不是用一组相似的值来初始化某些东西,那么您就不应该将它们作为函数参数传递。很难理解为什么foo需要使用初始化列表。它不是一个容器;这正是您所链接的答案试图提醒您的内容。如果您想要一个数组,那就不同于想要一个初始化列表。 - Nicol Bolas
3
“@Potatoswatter:“你怎么知道foo不是构造一个对象并将其存储在某个地方?”我不知道。我的观点是,你不清楚为什么foo要采用初始化列表。对我来说,唯一应该采用带有std::initializer_list参数的函数类型是对象的构造函数。所有其他函数都应该采用具有实际语义的容器形式。” - Nicol Bolas
4
“无论如何,对于容器来说,initializer_list的语义也是错误的,这就是重点。” 这取决于你使用它们的目的。如果它是一个整数数组,那么没有问题。如果它是一个更重的对象数组,那么使用错误的工具就是你自己的问题。此外,你只能得到一份拷贝;你可以将内容移动到initializer_list对象的存储中。与任何其他由用户选择的移动一样,你必须使用std::move来调用它。 - Nicol Bolas
3
啊...你可以将内容移动到初始化列表中,这样只需要复制一次。但是对初始化列表应用move是没有意义的。 - Potatoswatter
4
啊,又是评论警察。 - TemplateRex
显示剩余17条评论
1个回答

6
如果需要方便的语法(例如:用户只需输入一个没有函数调用或单词的{}列表),那么您必须接受适当的initializer_list的所有权力和限制。即使您试图将其转换为其他形式,例如某种array_ref,您仍然需要在它们之间有一个中介的initializer_list。这意味着您无法避免任何问题,比如无法从中移出。
如果经过initializer_list,则必须接受这些限制。因此,替代方法是不要通过initializer_list,这意味着您将不得不接受具有特定语义的某种容器。而且替代类型必须是聚合的,以便替代对象的构造不会遇到相同的问题。
因此,您可能正在强制用户创建一个std::array(或语言数组)并传递该数组。您的函数可以采用某种array_ref类,该类可以从任何大小的数组构造,因此使用函数的消费者不受限于一种大小。
但是,您会失去大小的方便性:
foo( { "blah", 3, dog } );

对比。

foo( std::array<bar, 3>{ "blah", 3, dog } );

避免冗长的方法是将 foo 作为参数采用 std :: array 。这意味着它只能采用特定固定大小的数组。您不能使用C ++ 14提出的 dynarray ,因为它将使用一个 initializer_list 中介。
最终,不应该使用统一初始化语法来传递值列表。它是用于初始化对象而不是传递事物列表。 std :: initializer_list 是一个类,其唯一目的是从具有相同类型的任意长度的值列表中用于初始化特定对象。它用作语言结构(大括号初始化列表)和将这些值馈入的构造函数之间的中介对象。当给定匹配的大括号初始化列表时,它允许编译器知道调用特定的构造函数(初始化列表构造函数)。
这就是该类存在的全部原因。
因此,您应仅将该类用于设计它的目的。该类存在标记构造函数,以从大括号初始化列表中获取一系列值。因此,仅应将其用于采用此类值的构造函数。
如果您有一些函数 foo ,它充当某些内部类型(您不想直接公开)和用户提供的值列表之间的中介,则需要采用其他参数将其传递给 foo 。具有所需语义的内容,然后将其馈入内部类型。
此外,您似乎对 initializer_list 和移动存在误解。您无法从 initializer_list 中移动,但是您当然可以移动到其中:
foo( { "blah", 3, std::move(dog) } );

内部dog数组的第三个条目将被移动构造。

我认为这里重要的问题是“您无法移出initializer_list”部分。 - Mark Garcia
@Potatoswatter,有使用初始化列表的工厂函数习惯用法的链接吗? - TemplateRex
3
关于 "initializer_list只能用作构造函数的参数" 的争议,我说的是“应该”,而不是"可以"或"可能"。我回答的是你应该做什么,而不是你能做什么。符合惯用法的C++代码应避免在构造函数之外使用initializer_list。存在一种工厂函数模式,它根本不能与统一初始化一起使用,那么你的观点是什么? - Nicol Bolas
1
@Potatoswatter:"如果您定义一个封装初始化列表构造函数功能的函数",您不能编写这样的函数,因此您的观点是无效的。 - Nicol Bolas
3
“Potatoswatter:我不敢相信你写了一整篇回答,就因为我的示例使用了通用的foo而不是构造函数。” 垃圾进, 垃圾出。如果你想得到一个特定问题的答案,那么你提供的示例需要真正代表你想知道的内容。如果你不打算在构造函数之外使用initializer_list,那么就不要提供一个使用它的示例。” - Nicol Bolas
3
@Potatoswatter 说:“你回答说语义很糟糕,所以很少应该使用它”。不,我回答说,对于 它预期的用途 来说,语义是好的,因此你应该限制自己只在这些预期的用途中使用它。也就是说,用它来构建东西。 - Nicol Bolas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接