c++typesheap-memorystack-memorystdinitializerlist

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


I understand from here that std::initializer_list doesn't need to allocate heap memory. Which is very strange to me since you can take in an std::initializer_list object without specifying the size whereas for arrays you always need to specify the size. This is although initializer lists internally almost the same as arrays (as the post suggests).

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 object must be fixed at compile time. Thus, every std::array is another type and we just spawn those types from a common template. But for std::initializer_list, this rule apparently doesn't apply, as the memory layout (while it can be derived from the arguments passed to its constructor) doesn't need to be considered for a receiving function or constructor. This makes sense to me only if the type heap allocates memory and only reserves storage to manage that memory. Then the difference would be much like std::array and std::vector, where for the later you also don't need to specify size.

Yet std::initializer_list doesn't use heap allocations, as my tests show:

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

How is this possible? Can I write a similar implementation for my own type? That would really save me from blowing my binary whenever I play the compile time orchestra.

EDIT: I should note that the question I wanted to ask is not how the compiler knows what size it should instantiate the initializer list with (as can be achieved via template argument deduction) but how it is then not a different type from all the other instantiations of an initializer list (hence why you can pass initializer lists of different sizes to the same function).


Solution

  • The thing is, std::initializer_list does not hold the objects inside itself. When you instantiate it, compiler injects some additional code to create a temporary array on the stack and stores pointers to that array inside the initializer_list. For what its worth, an initializer_list is nothing but a struct with two pointers (or a pointer and a size):

    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_; }
    
      // ...
    };
    

    When you do:

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

    Conceptually, here is what is happening:

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

    One minor difference being, the life-time of the array does not exceed that of the initializer_list.