2

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
glades
  • 3,778
  • 1
  • 12
  • 34
  • 4
    You should remove unrelated code. what you ask has no relation to `std::variant`. – apple apple Jul 16 '22 at 09:46
  • 1
    Let's put it this way: I would only ever use a `std::initializer_list` while it is in scope, just like any other local variable. – Paul Sanders Jul 16 '22 at 10:22
  • 1
    *Storing* the list may not be UB but accessing its members after the 'source' has gone out of scope almost certainly is. – Adrian Mole Jul 16 '22 at 10:42
  • @AdrianMole Well that is my question. _Is_ the source out of scope when the container constructor is executed? – glades Jul 16 '22 at 12:37
  • temporary lifetime end at full expression. – apple apple Jul 16 '22 at 13:29
  • @appleapple No it doesn't answer it. The full expression of the outermost call to the constructor of container ends after the constructor has been called, yes. So the outermost initializer list will be fine and can be used by the containers constructor. But when do the expressions end for the calls that go out to the constructors of the inner elements of the brace-enclosed initializer lists? Can I still use them in my container constructor or will they be out of scope already? – glades Jul 16 '22 at 13:46
  • @glades I don't get what you're saying. there is no inner call afaict? – apple apple Jul 16 '22 at 14:05
  • @glades you should provide [mcve], not something that involve `std::variant` and a lot of overload and conversions so we (and yourself) can better understand your think. – apple apple Jul 16 '22 at 14:07
  • @glades fwiw, in your own example, it's pretty clear that all object are constructed before you ever enter the `container`'s constructor. – apple apple Jul 16 '22 at 14:17

1 Answers1

2

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

apple apple
  • 10,292
  • 2
  • 16
  • 36
  • Thank you but I still got a question: Let's enumerate the constructors taking initializer_lists by level: 1,2 and 3 whereas 3 is the constructor which takes an initializer list containing "itemA" and 2. Now before constructor 3 is called, a temporary array of "itemA" and 2 is preallocated on the stack and its initializer_list is then passed to constructor 3 which stores it in its member. But this happens at scope of constructor 2 which terminates after the last "initializer_list stored" is printed. Isn't now the temporary array mentioned before officialy out of scope and therefore invalid? – glades Jul 18 '22 at 08:21
  • @glades no, the array also goes out of scope after the full expression `;` (as can be seen in the linked example. all destructor are only called after the constructor of `container` runs). – apple apple Jul 18 '22 at 14:15
  • i.e. all `std::initializer_list` and their underlying arrays and all `ref` are only destoryed after the full expression ends. – apple apple Jul 18 '22 at 14:17
  • @glades if you're refer to the `std::initializer_list` pass by value in the constructor. it does goes out of scope. but the original `std::initializer_list` that created before passed to the constructor and the underlying object is not, and the copy (which you stored) can still access it no problem. – apple apple Jul 18 '22 at 14:21
  • @glades btw, re *a temporary array of "itemA" and 2 is preallocated on the stack*, no, you're passing a `std::initializer_list` so the array is actually `{ref{"itemA"},ref{1}}` – apple apple Jul 18 '22 at 14:26
  • Ahh so that means that the original initializer_list is never placed in scoped context (as I suspected is always the case for automatic variables) and waits for the full expression to return? Also can you put that bit in your answer it's really helpful! – glades Jul 18 '22 at 16:48