Search code examples
c++initializer-list

C++ std::initializer_list usage


I have a couple of questions about how initializer_list`s work in C++. First of all, say that we have (std:: omitted for simplicity):

initializer_list<uint32_t> lst({ 5, 4, 3 }); // (*)

In Visual Studio, I peaked a bit into the class and found only these two constructors:

constexpr initializer_list() noexcept : _First(nullptr), _Last(nullptr) {}

constexpr initializer_list(const _Elem* _First_arg, const _Elem* _Last_arg) noexcept
    : _First(_First_arg), _Last(_Last_arg) {}

So my first question is: the line above marked with (*), is there some kind of syntactic sugar behind that allows this syntax (or maybe regular syntax for the compiler, so that he mangles with it and sets up everything properly)?

By the look of the class, everything is constexpr so my conclusion is that it's all done in compile time, but I couldn't find any concrete explanations about the question.

Next question is a bit contradictory to the previous conclusion, but since I am not 100% sure about the stuff happening, I'm gonna ask anyway: if I'm not explicitly using new initializer_list<int>({...});, can the initializer_list ever use dynamic memory in any way (is there a certain use-case where it does)?

Again, it's a silly question, but I want to understand exactly what happens with the class, as well as how it mangles with the memory.

Third question is, say that we have a code that looks like this:

#include <iostream>
using namespace std;

class Test
{
    public: Test(initializer_list<uint32_t>&& lst)
    {
        cout << "Size: " << lst.size() << endl;

        for (const uint32_t* i = lst.begin(); i != lst.end(); ++i)
        {
            cout << " " << *i;
        }

        cout << endl << endl;
    }
};

int main(void)
{
    Test t1({ 4, 6, 3 });
    Test t2({ 6, 3, 2, 8 });

    return 0;
}

In this case, is there any possible way to use the constructor of the Test class other than the way it is used with the t1 and t2 instances? And of course, is this even correct to begin with? cout does print out the correct values, however I want to be sure that there is nothing ill-formed, like for instance the 'brace init list' { ... } being destroyed (if saying something like this is even possible) before cout uses them?

This is bugging me since I don't have any understanding about how does the C++ manage the memory when things like these are written.


Solution

  • 1

    So my first question is: the line above marked with (*), is there some kind of syntactic sugar behind that allows this syntax (or maybe regular syntax for the compiler, so that he mangles with it and sets up everything properly)?

    From cppreference:

    Notes

    Despite a lack of constructors, it is possible to create non-empty initializer lists. Instances of std::initializer_list are implicitly constructed when:

    • a braced-init-list is used in list-initialization, including function-call list initialization and assignment expressions (not to be confused with constructor initializer lists)
    • a braced-init-list is bound to auto, including in a ranged for loop

    2

    Next question [...]: if I'm not explicitly using new initializer_list({...});, can the initializer_list ever use dynamic memory in any way (is there a certain use-case where it does)?

    Why would you? Their main purpose is to pass a list of parameters to constructors. They are a lightweight wrapper around an array that is constructed by the compiler on the fly to allow you write code along the line of

    std::string s1{'a', 'b', 'c', 'd'};
    

    3a

    In this case, is there any possible way to use the constructor of the Test class other than the way it is used with the t1 and t2 instances?

    I do not really understand what you are asking here. I would write it like this:

    Test t1{ 4, 6, 3 };
    Test t2{ 6, 3, 2, 8 };
    

    3b

    And of course, is this even correct to begin with?

    There is nothing wrong in your code.

    3c

    like for instance the 'brace init list' { ... } being destroyed (if saying something like this is even possible) before cout uses them?

    Again from cppreference:

    The underlying array is a temporary array of type const T[N], in which each element is copy-initialized (except that narrowing conversions are invalid) from the corresponding element of the original initializer list. The lifetime of the underlying array is the same as any other temporary object, except that initializing an initializer_list object from the array extends the lifetime of the array exactly like binding a reference to a temporary (with the same exceptions, such as for initializing a non-static class member). The underlying array may be allocated in read-only memory.

    TL;DR: Even temporaries dont cease to exists between two random lines of code. You can safely use the function parameter lst till the end of the function.

    PS

    Only while half-way through this answer I realized that it is not only 3 but actually even more questions. Better focus on one point in a question. If you have more, open more questions.