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.