Search code examples
c++c++17undefined-behaviorinitializer-listvariant

Is storing initializer_lists undefined behaviour?


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

The short answer was that calling a function with brace-enclosed list foo({2, 3, 4, 5, 6}); conceptually creates a temporary array in the stackspace before the call and then passes the initializer list which (like string_view) just references this local temporary array (probably in registers):

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

Now consider the following case where I have nested initializer_lists of an object "ref". This ref object stores primitives types or an initializer_list recursively in a variant. My question now is: Is this undefined behaviour? It appears to work with my code, but does it hold up to the standard? My reason for doubting is that when the inner constructor calls for the nested brace-enclosed lists return, the temporary array which the initializer list is refering to could be invalidated because the stack pointer is reset (thus saving the initializer_list in the variant preserves an invalid object). Writing to subsequent memory would then overwrite values refered to by the initializer list. Am I wrong in believing that?

CompilerExplorer

#include <variant>
#include <string_view>
#include <type_traits>
#include <cstdio>

using val = std::variant<std::monostate, int, bool, std::string_view, std::initializer_list<struct ref>>;

struct ref
{
    ref(bool);
    ref(int);
    ref(const char*);
    ref(std::initializer_list<ref>);

    val value_;
};

struct container
{
    container(std::initializer_list<ref> init) {
        printf("---------------------\n");
        print_list(init);
    }

    void print_list(std::initializer_list<ref> list)
    {
        for (const ref& r : list) {
            if (std::holds_alternative<std::monostate>(r.value_)) {
                printf("int\n");
            } else if (std::holds_alternative<int>(r.value_)) {
                printf("int\n");
            } else if (std::holds_alternative<bool>(r.value_)) {
                printf("bool\n");
            } else if (std::holds_alternative<std::string_view>(r.value_)) {
                printf("string_view\n");
            } else if (std::holds_alternative<std::initializer_list<ref>>(r.value_)) {
                printf("initializer_list:\n");
                print_list(std::get<std::initializer_list<ref>>(r.value_));
            }
        }
    }
};

ref::ref(int init) : value_{init} { printf("%d stored\n", init); }
ref::ref(bool init) : value_{init} { printf("%s stored\n", init ? "true" : "false"); }
ref::ref(const char* init) : value_{std::string_view{init}} { printf("%s stored\n", init); }
ref::ref(std::initializer_list<ref> init) : value_{init} { printf("initializer_list stored\n", init); }

int main()
{
    container some_container = { 1, true, 5, { {"itemA", 2}, {"itemB", true}}};
}

Output:

1 stored
true stored
5 stored
itemA stored
2 stored
initializer_list stored
itemB stored
true stored
initializer_list stored
initializer_list stored
---------------------
int
bool
int
initializer_list:
initializer_list:
string_view
int
initializer_list:
string_view
bool

Solution

  • First of all, copying a std::initializer_list does not copy the underlying objects. (cppreference)


    and

    container some_container = { 1, true, 5, { {"itemA", 2}, {"itemB", true}}};
    

    actually compiles to something like

    container some_container = { // initializer_list<ref>
       ref{1}, 
       ref{true}, 
       rer{5},
       ref{ // initializer_list<ref>
          ref{ // initializer_list<ref>
             ref{"itemA"}, 
             ref{2}
          }, 
          ref{ // initializer_list<ref>
             ref{"itemB"},
             ref{true}
          }
       }
    };
    

    and all those object's lifetime* end at end of full expression (the ; here)

    *including all initializer_list<ref>, their underlying array, and all the belonging ref objects, but not the one in the constructor (copy elision may apply though)


    so

    1. yes it's fine to use those object in the constructor.
    2. those object are gone after the ; so you should not use the stored object (in initializer_list) anymore

    godbolt example with noisy destructor and action after the construction