为啥 std::initializer_list 不是内置语言?

Posted

技术标签:

【中文标题】为啥 std::initializer_list 不是内置语言?【英文标题】:Why isn't std::initializer_list a language built-in?为什么 std::initializer_list 不是内置语言? 【发布时间】:2013-02-18 09:05:24 【问题描述】:

为什么std::initializer_list 不是内置的核心语言?

在我看来,它是 C++11 的一个非常重要的特性,但它没有自己的保留关键字(或类似的东西)。

相反,initializer_list只是标准库中的一个模板类,它具有来自新 braced-init-list映射 /em> ... 由编译器处理的语法。

乍一看,这个解决方案很hacky

现在 C++ 语言的新增功能是这样实现的吗:通过某些模板类的隐式角色,而不是通过核心语言?


请考虑以下示例:

   widget<int> w = 1,2,3; //this is how we want to use a class

为什么选择了一个新班级:

   widget( std::initializer_list<T> init )

不要使用与以下任何想法类似的东西:

   widget( T[] init, int length )  // (1)
   widget( T... init )             // (2)
   widget( std::vector<T> init )   // (3)
    一个经典的数组,你可以在这里和那里添加const 语言中已经存在三个点(var-args,现在是可变参数模板),为什么不重用语法(让它感觉内置) 只是一个现有的容器,可以添加const&amp;

它们都已经是语言的一部分。我只写了我最初的 3 个想法,我相信还有许多其他方法。

【问题讨论】:

标准委员会讨厌添加新关键字! 这个我明白了,但是如何扩展语言有很多可能性(关键字只是一个例子 std::array&lt;T&gt; 不比std::initializer_list&lt;T&gt; 更“语言的一部分”。这些几乎不是该语言所依赖的唯一库组件。见new/deletetype_info、各种异常类型、size_t @Elmes:我会建议const T(*)[N],因为它的行为与std::initializer_list 的工作方式非常相似。 This 回答了为什么 std::array 或静态大小的数组不太理想。 【参考方案1】:

已经有一些“核心”语言功能示例返回在std 命名空间中定义的类型。 typeid 返回 std::type_info 并且(可能会拉伸一点)sizeof 返回 std::size_t

在前一种情况下,您已经需要包含一个标准标题才能使用这个所谓的“核心语言”功能。

现在,对于初始化列表,生成对象不需要关键字,语法是上下文相关的花括号。除此之外,它与type_info 相同。就我个人而言,我不认为缺少关键字会使其“更加hacky”。也许更令人惊讶,但请记住,目标是允许聚合已经允许的相同的花括号初始化器语法。

所以是的,您可能会期待更多这种设计原则:

如果出现更多可以在没有新关键字的情况下引入新功能的情况,那么委员会将采用它们。 如果新功能需要复杂类型,那么这些类型将被放置在 std 中,而不是作为内置函数。

因此:

如果一个新特性需要一个复杂的类型并且可以在没有新关键字的情况下引入,那么你会在这里得到你所拥有的,这是没有新关键字的“核心语言”语法,它使用来自std的库类型。

我认为归根结底是,在 C++ 中,“核心语言”和标准库之间没有绝对的区别。它们是标准中的不同章节,但相互引用,而且一直如此。

在 C++11 中还有另一种方法,即 lambda 引入具有由编译器生成的 匿名 类型的对象。因为它们没有名称,所以它们根本不在命名空间中,当然不在std 中。但是,这不是初始化列表的合适方法,因为您在编写接受类型的构造函数时使用类型名称。

【讨论】:

在我看来,这种划分是不可能的(mailny?)因为类型的这种 implicit 角色。 type_infosize_t 是很好的论据.. 好吧 size_t 只是一个 typedef.. 所以让我们跳过这个。 type_infoinitializer_list 之间的区别在于,第一个是 explicit 运算符的结果,而第二个是 implicit 编译器操作的结果。在我看来,initializer_list 可以替换为一些已经存在的容器。或者更好:任何用户声明为参数类型! ... 或者这可能是一个简单的原因,如果您为 vector 编写了一个带有 array 的构造函数,那么您可以从 any 数组构造一个向量正确的类型,而不仅仅是由初始化列表语法生成的类型。我不确定从任何array 构造容器是否是一件坏事,但这不是委员会引入新语法的意图。 @Christian:不,std::array 甚至没有任何构造函数。 std::array 案例只是聚合初始化。另外,欢迎大家加入 Lounge 聊天室,因为这个讨论有点长。 @ChristianRau:Xeo 表示在构造初始化列表时复制元素。复制初始化列表不会复制包含的元素。 @Christian List-initialisation 并不意味着 initializer_list。它可以是很多东西,包括良好的 ole 直接初始化或聚合初始化。这些都不涉及initializer_list(有些只是不能那样工作)。【参考方案2】:

C++ 标准委员会似乎不喜欢添加新的关键字,可能是因为这会增加破坏现有代码的风险(旧代码可以使用该关键字作为变量、类或其他任何名称的名称)。

此外,在我看来,将std::initializer_list 定义为模板化容器是一个非常优雅的选择:如果它是一个关键字,你将如何访问它的底层类型?你将如何迭代它?您还需要一堆新的运算符,而这只会迫使您记住更多的名称和更多的关键字来执行与标准容器相同的操作。

std::initializer_list 视为任何其他容器,让您有机会编写适用于任何这些东西的通用代码。

更新:

那为什么要引入一种新类型,而不是使用现有的某种组合呢? (来自 cmets)

首先,所有其他容器都有添加、删除和放置元素的方法,这对于编译器生成的集合来说是不可取的。唯一的例外是 std::array&lt;&gt;,它包装了一个固定大小的 C 样式数组,因此仍然是唯一合理的候选者。

然而,正如 Nicol Bolas 在 cmets 中正确指出的那样,std::initializer_list 和所有其他标准容器(包括 std::array&lt;&gt;)之间的另一个根本区别是后者具有 值语义 ,而std::initializer_list 具有引用语义。例如,复制std::initializer_list 不会导致复制它所包含的元素。

此外(再次由 Nicol Bolas 提供),拥有一个用于大括号初始化列表的特殊容器允许用户执行初始化的方式重载。

【讨论】:

那为什么要引入新类型,而不是使用现有的某种组合呢? @elmes:其实它更像std::array。但是std::array 分配内存,而std::initializaer_list 包装了一个编译时数组。将其视为char s[] = "array";char *s = "initializer_list"; 之间的区别。 并且将其作为一个 normal 类型使得重载、模板专业化、名称装饰等都不是问题。 @rodrigo: std::array 不分配任何内存,它是一个普通的T arr[N];,与支持std::initializer_list 的东西相同。 @Xeo: T arr[N] 确实分配了内存,可能不在动态堆中,而是在其他地方......std::array也是如此。但是,用户无法构造非空的initializer_list,因此它显然无法分配内存。【参考方案3】:

这不是什么新鲜事。例如,for (i : some_container) 依赖于 some_container 类中的 existence of specific methods or standalone functions。 C# 甚至更多地依赖于它的 .NET 库。实际上,我认为这是一个非常优雅的解决方案,因为您可以使您的类与某些语言结构兼容,而不会使语言规范复杂化。

【讨论】:

类中的方法独立的beginend方法。这与 IMO 有点不同。 是吗?同样,您有一个纯语言结构,依赖于您的代码的特定结构。也可以通过引入新关键字来完成,例如,iterable class MyClass ; 但是你可以把方法放在任何你想要的地方,按照你想要的方式实现它们。有一些相似之处,我同意!这个问题是关于initializer_list 虽然【参考方案4】:

这确实不是什么新鲜事,有多少人指出,这种做法在 C++ 中就有,在 C# 中也有。

不过,Andrei Alexandrescu 提到了一个很好的观点:您可以将其视为想象中的“核心”命名空间的一部分,那么它会更有意义。

所以,它实际上类似于:core::initializer_listcore::size_tcore::begin()core::end() 等等。这只是一个不幸的巧合,std 命名空间内部有一些核心语言结构。

【讨论】:

【参考方案5】:

它不仅可以在标准库中完全工作。包含到标准库中并不意味着编译器不能玩聪明的把戏。

虽然它可能无法在所有情况下,但它很可能会说:这种类型是众所周知的,或者是一个简单的类型,让我们忽略initializer_list,只需要一个初始化值应该是什么的内存映像.

换句话说,int i 5; 可以等价于int i(5);int i=5; 甚至intwrapper iw 5; 其中intwrapper 是一个简单的包装类,它带有一个简单的构造函数,采用initializer_list@

【讨论】:

我们是否有可重现的编译器实际上在玩这样的“聪明把戏”的例子?在 as-if 下这有点道理,但我希望看到证据。 优化编译器的想法是编译器可以将代码转换为任何等效代码。 C++ 尤其依赖于“免费”抽象的优化。从标准库替换代码的想法很常见(查看 gcc 内置列表gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)。 事实上,您认为int i 5 涉及任何std::initializer_list 的想法是错误的。 int 没有采用std::initializer_list 的构造函数,所以5 只是直接用于构造它。所以主要的例子是无关紧要的;根本不需要优化。除此之外,由于std::initializer_list 涉及编译器创建和代理一个“虚构”数组,我想它可以有利于优化,但这是编译器中的“神奇”部分,因此它与优化器是否可以做任何聪明的事情是分开的带有包含 2 个迭代器的相当沉闷的对象,结果【参考方案6】:

它不是核心语言的一部分,因为它可以完全在库中实现,只需行operator newoperator delete。让编译器更复杂地构建它有什么好处?

【讨论】:

以上是关于为啥 std::initializer_list 不是内置语言?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 std::min(std::initializer_list<T>) 按值接受参数?

为啥具有默认参数 std::initializer_list 的 ctor 不可用(VS2019)?

为啥在使用大括号初始值设定项列表时首选 std::initializer_list 构造函数?

为啥我的编译器无法计算出这种转换,它何时存在?

std::initializer_list 作为函数参数

如何将 C 数组转换为 std::initializer_list?