10

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?

TylerH
  • 20,799
  • 66
  • 75
  • 101
Viper Bailey
  • 11,518
  • 5
  • 22
  • 33
  • Hmm. This [cppreference page](https://en.cppreference.com/w/cpp/utility/initializer_list) has a paragraph in especially garbled legalese that *seems* to suggest what you're doing is safe: *The lifetime of the underlying array is the same as any other temporary object, except that initializing an initializer_list object from the array extends the lifetime of the array exactly like binding a reference to a temporary (with the same exceptions, such as for initializing a non-static class member).* – Adrian Mole May 03 '22 at 18:30
  • Maybe we need the `language-lawyer` tag to get someone to dissect that paragraph? – Adrian Mole May 03 '22 at 18:34
  • 3
    [Related? Duplicate?](https://stackoverflow.com/q/15286450/10871073) – Adrian Mole May 03 '22 at 18:40
  • I saw that question prior to posting mine, but we're talking about two different things. In that case they are actually returning the std::initializer_list whereas in my example, it is all held internal to the given expression. As for the cppreference page, yeah... I read that a few times before posting my question and couldn't make heads or tails of it. – Viper Bailey May 03 '22 at 19:35

2 Answers2

2

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.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • This is true in the case of the first constructor, but won't the use of `has_list` protect the class from depending on that undefined behavior? – Derek T. Jones May 04 '22 at 17:27
  • @DerekT.Jones No, the constructors aren't even relevant. Even `struct wrapped { std::initializer_list lst; };` would have undefined behavior due to the stated reason. However, this is probably just a defect in the standard. I don't think there is any reason that `std::initializer_list` should be implemented in such a way that this would be a problem. However, the question is tagged `language-lawyer`, so I am giving a pedantic answer. – user17732522 May 04 '22 at 18:29
1

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.

Derek T. Jones
  • 1,800
  • 10
  • 18