为什么允许 std::initializer_list 不指定大小并同时分配堆栈?

How come std::initializer_list is allowed to not specify size AND be stack allocated at the same time?

我从here了解到std::initializer_list不需要分配堆内存。这对我来说很奇怪,因为您可以在不指定大小的情况下接收 std::initializer_list 对象,而对于数组,您总是需要指定大小。这是尽管初始化列表在内部几乎与数组相同(如 post 所建议的)。

我很难理解的是,C++ 作为一种静态类型语言,每个对象的内存布局(和大小)必须在编译时固定。因此,每个 std::array 都是另一个 type,我们只是从一个通用模板中生成这些类型。但对于 std::initializer_list,此规则显然不适用,因为内存布局(虽然它可以从传递给其构造函数的参数派生)不需要为接收函数或构造函数考虑。仅当类型堆分配内存并且仅保留存储以 manage 该内存时,这对我才有意义。那么差异就很像 std::array 和 std::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";
}

这怎么可能?我可以为我自己的类型编写一个类似的实现吗?每当我演奏编译时管弦乐队时,这真的可以避免我的二进制文件崩溃。

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

问题是,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});

一个小区别是,数组的 life-time 不超过 initializer_list。

... whereas for arrays you always need to specify the size ...

你的意思是喜欢

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

?

What I have a hard time wrapping my head around is that with C++ as a statically typed language, the memory layout (and size) of every must be fixed at compile time.

初始化列表的大小与上面数组的大小一样在编译时固定——它是固定的因为你写了大括号表达式{2, 3, 2, 6, 7}在编译之前明确。

How is this possible? Can I write a similar implementation for my own type?

你不能拦截一个braced-init-list的解析,不行。你可以 see,处理列表初始化的规则非常具体。

然而,std::initializer_list 旨在实现轻量级,因此您可以直接使用它。正如另一个答案所说,您可以将其视为普通 implicitly-sized 数组,并隐式转换为 range-like 视图。

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

如果您查看相应的 STL 头文件,您会找到一个定义 API 的定义。但是实际的实现实际上是编译器内置的,所以当你写的时候,说:

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

编译器运行“a-ha!一个初始化列表(以及 int 的列表,启动),我知道那是什么,我将构建一个”。确实如此。 STL 本身没有执行此操作的代码。

换句话说,对于编译器来说,std::initializer_list 部分是本机类型。只是它不是,不完全是,因此它是 独一无二的 非常 select 俱乐部的成员(请参阅评论)。

您似乎有一个误解是您需要一个在编译时已知的大小才能在堆栈上分配。事实上,没有技术原因 禁止在运行时从堆栈中确定大小的对象。事实上,C 用 Variable Length Arrays. There's another question on this topic 明确允许它,尽管它询问 C.

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

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