Lifetime of std::initializer_list when used recursively

Viewed 159

I am trying to use std::initializer_list in order to define and output recursive data-structures. In the example below I am dealing with a list where each element can either be an integer or another instance of this same type of list. I do this with an intermediate variant type which can either be an initializer list or an integer.

What is unclear to me is whether the lifetime of the std::initializer_list will be long enough to support this use-case or if I will be experiencing the possibility of inconsistent memory access. The code runs fine, but I worry that this is not guaranteed. My hope is that the std::initializer_list and any intermediate, temporary std::initializer_list objects used to create the top-most list are only cleaned up after the top-level expression is complete.

struct wrapped {
    bool has_list;
    int n = 0;
    std::initializer_list<wrapped> lst;

    wrapped(int n_) : has_list(false), n(n_) {}
    wrapped(std::initializer_list<wrapped> lst_) : has_list(true), lst(lst_) {}

    void output() const {
      if (!has_list) {
        std::cout << n << ' ';
      } else {
        std::cout << "[ ";
        for (auto&& x : lst)  x.output();
        std::cout << "] ";
      }
    }
  };

  void call_it(wrapped w) {
    w.output();
    std::cout << std::endl;
  }

  void call_it() {
    call_it({1});                 // [ 1 ]
    call_it({1,2, {3,4}, 5});     // [ 1 2 [ 3 4 ] 5 ]
    call_it({1,{{{{2}}}}});       // [ 1 [ [ [ [ 2 ] ] ] ] ]
  }

Is this safe, or undefined behavior?

2 Answers

As far as I can tell the program has undefined behavior.

The member declaration std::initializer_list<wrapped> lst; requires the type to be complete and hence will implicitly instantiate std::initializer_list<wrapped>.

At this point wrapped is an incomplete type. According to [res.on.functions]/2.5, if no specific exception is stated, instantiating a standard library template with an incomplete type as template argument is undefined.

I don't see any such exception in [support.initlist].

See also active LWG issue 2493.

What you're doing should be safe. Section 6.7.7 of the ISO Standard, paragraph 6, describes the third and last context in which temporaries are destroyed at a different point than the end of the full expression. Footnote (35) explicitly says that this applies to the initialization of an intializer_list with its underlying temporary array:

The temporary object to which the reference is bound or the temporary object that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference if the glvalue to which the reference is bound was obtained through one of the following: ...

...(where a glvalue is, per 7.2.1, an expression whose evaluation determines the identity of an object, bit-field, or function) and then enumerates the conditions. In (6.9), it specifically mentions that:

A temporary object bound to a reference parameter in a function call (7.6.1.2) persists until the completion of the full-expression containing the call.

As I read it, this protects everything needed to build up the final argument to the top call to call_it, which is what you intend.

Related