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

Time:05-25

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 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.

CodePudding user response:

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

You mean like for

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.

The size of the initializer list is just as fixed at compile time as the size of the array above - it's fixed because you wrote the braced expression {2, 3, 2, 6, 7} out explicitly before compiling.

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

You can't intercept the parsing of a braced-init-list, no. As you can see, the rules for handling list initialization are pretty specific.

However, the std::initializer_list is intended to be lightweight so you can use it directly. As the other answer says, you can consider it as a normal implicitly-sized array with an implicit conversion to a range-like view.

CodePudding user response:

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.

CodePudding user response:

Just to add a few random musings of my own, std::initializer_list is actually an interesting animal - it's a kind of chimera, part STL, part compiler construct.

If you look in the appropriate STL header file, you will find a definition for it that defines the API. But the actual implementation is, in effect, built into the compiler, so when you write, say:

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

The compiler goes "a-ha! An initializer list (and a list of int's, to boot), I know what that is, I'll construct one". And so it does. There's no code to do that in the STL itself.

To put it another way, to the compiler, std::initializer_list is, in part, a native type. Only it's not, not completely, and as such it is unique.

  • Related