为什么std::initializer_list可以不指定大小并同时进行堆栈分配



我从这里了解到std::initializer_list不需要分配堆内存。这对我来说很奇怪,因为你可以在不指定大小的情况下接收std::initializer_list对象,而对于数组,你总是需要指定大小。尽管初始值设定项在内部列出的内容与数组几乎相同(正如帖子所示)。

我很难理解的是,C++作为一种静态类型语言,每个对象的内存布局(和大小)必须在编译时固定。因此,每个std::array都是另一个类型,我们只是从一个公共模板中派生这些类型。但对于std::initializer_list,这条规则显然不适用,因为接收函数或构造函数不需要考虑内存布局(虽然它可以从传递给其构造函数的参数中派生)。只有当类型堆分配内存并且只保留存储以管理该内存时,这对我来说才有意义。那么差异将非常类似于std::arraystd::vector,其中对于后面的内容,您也不需要指定大小。

然而std::initializer_list并没有使用堆分配,正如我的测试所示:

#include <string>
#include <iostream>
void* operator new(size_t size)
{
std::cout << "new overload called" << std::endl;    
return malloc(size);
}

template <typename T>
void foo(std::initializer_list<T> args)
{
for (auto&& a : args)
std::cout << a << std::endl;
}
int main()
{
foo({2, 3, 2, 6, 7});
// std::string test_alloc = "some string longer than std::string SSO";
}

这怎么可能?我可以为自己的类型编写类似的实现吗?这真的可以让我在任何时候演奏编译时管弦乐队时都不用吹二进制文件。

EDIT:我应该注意,我想问的问题不是编译器如何知道它应该用什么大小实例化初始化器列表(这可以通过模板参数推导来实现),而是它如何与初始化器列表的所有其他实例化不是不同的类型(因此,为什么可以将不同大小的初始化器列表传递给同一函数)。

问题是,std::initializer_list本身并不包含对象。当您实例化它时,编译器会注入一些额外的代码来在堆栈上创建一个临时数组,并将指向该数组的指针存储在initializer_list中。就其价值而言,initializer_list只不过是一个有两个指针(或一个指针和一个大小)的结构:

template <class T>
class initializer_list {
private:
T* begin_;
T* end_;
public:
size_t size() const { return end_ - begin_; }
T const* begin() const { return begin_; }
T const* end() const { return end_; }
// ...
};

当你这样做:

foo({2, 3, 4, 5, 6});

从概念上,以下是正在发生的事情:

int __tmp_arr[5] {2, 3, 4, 5, 6};
foo(std::initializer_list{arr, arr + 5});

一个小的区别是,数组的生存时间不超过initializer_list的生存时间。

。。。而对于数组,您总是需要指定大小。。。

你是说像

int a[] = {2, 3, 2, 6, 7};

我很难理解的是,由于C++是一种静态类型的语言,每个语言的内存布局(和大小)都必须在编译时固定。

初始值设定项列表的大小在编译时与上面的数组大小一样固定-它是固定的,因为您在编译前显式写出了支撑表达式{2, 3, 2, 6, 7}

这怎么可能?我可以为自己的类型编写类似的实现吗?

您不能拦截对支持的init列表的解析。正如您所看到的,处理列表初始化的规则非常具体。

但是,std::initializer_list是轻量级的,因此您可以直接使用它。正如另一个答案所说,您可以将其视为一个正常的隐式大小的数组,并隐式转换为类似范围的视图。

只是添加一些我自己的随机沉思,std::initializer_list实际上是一种有趣的动物——它是一种嵌合体,部分是STL,部分是编译器构造。

如果在适当的STL头文件中查找,将找到一个定义API的定义。但实际的实现实际上是内置在编译器中的,所以当您编写时,可以说:

std::initializer_list <int> l = { 1, 2, 3, 4, 5 };

编译器执行";哈哈!一个初始值设定项列表(以及要启动的int的列表),我知道那是什么,我将构造一个";。事实的确如此。STL本身没有代码可以做到这一点。

换句话说,对于编译器来说,std::initializer_list在一定程度上是一种原生类型。只是它不是,不是完全的,因此它是独特的,是一个非常精选的俱乐部的成员(请参阅评论)。

您似乎有一个误解,即您需要在编译时已知的大小来在堆栈上分配。事实上,没有任何技术原因禁止在运行时从堆栈上确定大小的对象。事实上,C明确允许使用可变长度数组。关于这个话题还有另一个问题,尽管它问的是C.

虽然我不知道有什么合理的C++方法可以真正进行手动堆栈分配(alloca()是一个糟糕的想法,而不是真正的C++),但没有什么能阻止编译器为您做这件事。

堆栈分配非常简单——其他人可能会纠正我的错误,但据我所知,它可以归结为简单地增加堆栈指针寄存器中的值。

最新更新