Search code examples
c++embeddedinitializer-liststatic-initialization

Can I hold onto the address returned by std::initializer_list<T>::begin()?


I have a number of static instances of a class of data which hold onto arrays of integers, simplied in the following:

class ReadableIds
{
public:
  const int * ids;
  ReadableIds( const int * _ids ) : ids(_ids) { }

  // ... a bunch of methods that operate on ids, not germane to the discussion
};

I currently populate them in the following way at filescope:

foo.cpp:

//
static const int phase_one_ids[] = { 1, 10, 3, 2, -1 };
static ReadableIds phase_one( phase_one_ids );

static const int phase_two_ids[] = { 31, 11, 23, 542, 11, 88, -1 };
static ReadableIds phase_two( phase_two_ids );

However, I really want them to be defined inline for legibility reasons ( the class is more complicated with other arguments passed to the constructor)

static ReadableIds phase_one( { 1, 10, 3, 2, -1 }, ... other args );
static ReadableIds phase_two( { 31, 11, 23, 542, 11, 88, -1 }, ... other args );

The only way I can compile this is if I introduce a std::initializer_list:

class ReadableIds
{
public:
    ReadableIds(const std::initializer_list<int> & _ids ) : 
        ids(_ids.begin())  // take address of initializer_list !!
    {
    }
};

But my understanding is that the std::initializer_list is temporary, so holding the address to that data is wrong.

Question: Without allocating memory dynamically, is there a solution to this to allow better syntax? Doing a copy results in the static memory consumption, and the copy of the data, and in my use case that isn't viable.


Solution

  • No, you can't store the result of _ids.begin(), or rather: You can store it, but can't dereference it after the initialization of phase_two where it becomes a dangling pointer.

    Your requirements seem to be that

    1. ReadableIds doesn't actually store the elements itself.
    2. There is no need to add a named variable to store the elements.
    3. No dynamic storage duration for the elements either.

    Then the only way is to store the elements in a temporary array object. The problem is that temporaries are normally destroyed after full-expression in which they are manifested, which is not what you want.

    std::initializer_list construction can extend the lifetime to that of the std::initializer_list object itself, but if you don't want to name a variable you now didn't do anything but shift the problem to extending the lifetime of the temporary std::initializer_list object.

    The only way to extend the lifetime of a temporary object to that of a class object is to store a reference (in)to the temporary object directly in the class and initializing it immediately with the newly manifested temporary via braced aggregate initialization.

    So, you can do:

    // DON'T USE THIS!
    
    class ReadableIds
    {
    public:
        // must be aggregate, no constructors!
    
        std::initializer_list<int>&& _ids; // must be reference!
        // other aggregate elements
    };
    

    Now you may use the syntax

    // DON'T USE THIS!
    
    // must be braces!
    static ReadableIds phase_one{ { 1, 10, 3, 2, -1 },
        /*initializers for other aggregate elements*/ };
    

    and the array referenced by _ids will live as long as phase_one, but it won't be copied or have its lifetime extended if phase_one is copied/moved! A copy will refer to the same array and std::initializer_list object! Copying/moving std::initializer_list explicitly also wont copy/move or extend the lifetime of the underlying array!

    In particular, if phase_one wasn't a static storage duration object, then the array will not live until the end of the program, only ever until the end of the scope in which phase_one is declared.

    The lifetime extension also doesn't work if you use parentheses instead of braces for initialization of phase_one or if you insert a constructor.

    All in all I would not recommend this. It is very fragile and not worth it to avoid naming one extra variable. (I would also not be sure that compilers correctly implement this special case of nested lifetime extension.)

    Also, reconsider whether the first requirement really is necessary. It is easy enough to make the class a template that automatically deduces the correct size for an internal array.


    A slightly safer variation would be to use an array instead of std::initializer_list. In that case you need to template the class on the size of the array though:

    // DON'T USE THIS!
    
    template<std::size_t N>
    class ReadableIds
    {
    public:
        // must be aggregate, no constructors!
    
        int (&&_ids)[N]; // must be reference!
        // other aggregate elements
    };
    

    The requirements on the initialization still apply, but there is less potential on mistakenly copying _ids.