Home > Software engineering >  Lifetime of std::initializer_list when used recursively
Lifetime of std::initializer_list when used recursively

Time:05-04

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 ] ] ] ] ]
  }

CodePudding user response:

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.

CodePudding user response:

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.

  • Related