I raised an issue against GSL array_view recently (which is forming the basis for a propsal for an std::array_view) which led to a side discussion about when it is appropriate to use std::initializer_list and braced init lists. In that discussion Gaby Dos Reis said "any use of std::initializer_list in context other than constructing a container, in particular an array_view should be suspicious to a trained eye.". The issue comments weren't deemed the right place to continue discussion of that topic but I'm still left confused about why it should be. It doesn't seem to be true to me but if there's a good reason I'm not seeing I'd like to understand it.
This is where GSL array_view takes a different view and forbids construction of a GSL array_view from a temporary (by deleting the constructors that take a Container&&) or from an std::initializer_list (even a non-temporary). I understand the reasoning behind that decision since broken code like this would compile:auto badAv1 = array_view<const int>{{1, 2, 3}};// dangling reference to temporary initializer listauto badAv2 = array_view<const int>{makeVector()};// dangling reference to temporary vectorBut I don't see how that makes it suspicious to use temporary initializer lists in general.
In short: stop trying to use `initializer_list` like it's a compact way of writing a temporary array. It's for initializing objects, not for making temporary arrays.
I can understand your concern about the current proposal explicitly disallowing creating `array_view` from a temporary (via deleting the `Container&&` constructors). I'd rather that such constructors just be made explicit. But either way, constructing a view of something that is always temporary like `initializer_list`, is not a good thing.
On 18 October 2015 at 15:48, Nicol Bolas <jmck...@gmail.com> wrote:In short: stop trying to use `initializer_list` like it's a compact way of writing a temporary array. It's for initializing objects, not for making temporary arrays.does this include using initializer_list in for-range loops?for (auto i : {1, 3, 5, 7}) {std::cout << i;}am i right in thinking that this is using initializer_list's std::begin/end and not constructing a vector and using it's std::begin/end overloads?
I can understand your concern about the current proposal explicitly disallowing creating `array_view` from a temporary (via deleting the `Container&&` constructors). I'd rather that such constructors just be made explicit. But either way, constructing a view of something that is always temporary like `initializer_list`, is not a good thing.doesn't string_view have a std::string&& constructor? pretty sure i saw a thread about that the other day but can't find it now...
On Sunday, October 18, 2015 at 2:24:46 PM UTC-4, Sam Kellett wrote:
On 18 October 2015 at 15:48, Nicol Bolas <jmck...@gmail.com> wrote:In short: stop trying to use `initializer_list` like it's a compact way of writing a temporary array. It's for initializing objects, not for making temporary arrays.does this include using initializer_list in for-range loops?for (auto i : {1, 3, 5, 7}) {std::cout << i;}am i right in thinking that this is using initializer_list's std::begin/end and not constructing a vector and using it's std::begin/end overloads?
Yes, that's true.
The issue is not that the feature cannot possibly ever work. It's that promoting such use will inevitably lead unwary users to use it incorrectly.
I can understand your concern about the current proposal explicitly disallowing creating `array_view` from a temporary (via deleting the `Container&&` constructors). I'd rather that such constructors just be made explicit. But either way, constructing a view of something that is always temporary like `initializer_list`, is not a good thing.doesn't string_view have a std::string&& constructor? pretty sure i saw a thread about that the other day but can't find it now...
I don't know about a thread, but library fundamentals v1 doesn't have one. It only has a `const&` one.
so weird, just had another search for it and came up with nothing. i must be having some crazy dreamsi think the reasoning behind it was so a function returning a string could be passed as a parameter expecting a string_view:
void foo(const std::string_view &);std::string bar();foo(bar());
whether or not this is a good idea i'm not sure -- although if it is, does that mean it would be for array_view too? personally, i'm not entirely convinced the convenience of this is worth making this legal:std::string_view s = std::string{"hello"};but like you said this constructor doesn't appear to be there anyway!
It seems pretty obvious.
`initializer_list` is a non-modifiable range of elements. The storage for that range is created by the compiler and has a lifetime that is non-obvious to C++ programmers. Or better yet, C++ programmers should not have to concern themselves with the lifetime of an `initializer_list`'s storage.
If you restrict your usage of `initializer_list` to constructors of containers, then you will never have problems with such lifetime issues. This naturally includes functions that forward items to said constructors. So long as the destination within the callstack of the function is some kind of constructor, you'll be fine.
4.4 Syntax
In the EWG there were strong support for the idea of the sequence constructor, but initially no consensus about the syntax needed to express it. There was a strong preference for syntax to make the “special” nature of a sequence constructor explicit.
...
Based on extensive discussions, we prefer the X(initializer_list) design, because this “special compiler-recognized class name” approach
...
- The initializer_list type can be used for any argument that can accept an initializer list. For example int f(int, initializer_list, int) can accept calls such as f(1, {2,3}, 4). This eliminates the need for variable argument lists (… arguments) in many (most?) places.
I know we're all seasoned C++ programmers who've been burned by lifetime issues enough times that we are cautious of them but sometimes it seems like there's a little too much paranoia around things that fortunately are well specified as quite safe.
On Sunday, 18 October 2015 07:48:10 UTC-7, Nicol Bolas wrote:
It seems pretty obvious.
`initializer_list` is a non-modifiable range of elements. The storage for that range is created by the compiler and has a lifetime that is non-obvious to C++ programmers. Or better yet, C++ programmers should not have to concern themselves with the lifetime of an `initializer_list`'s storage.
If you restrict your usage of `initializer_list` to constructors of containers, then you will never have problems with such lifetime issues. This naturally includes functions that forward items to said constructors. So long as the destination within the callstack of the function is some kind of constructor, you'll be fine.Gaby made a similar claim and referred me to the original paper N2215 on the motivation for initializer lists being added to the language. That document however has this section (emphasis mine):4.4 Syntax
In the EWG there were strong support for the idea of the sequence constructor, but initially no consensus about the syntax needed to express it. There was a strong preference for syntax to make the “special” nature of a sequence constructor explicit.
...
Based on extensive discussions, we prefer the X(initializer_list) design, because this “special compiler-recognized class name” approach
...
- The initializer_list type can be used for any argument that can accept an initializer list. For example int f(int, initializer_list, int) can accept calls such as f(1, {2,3}, 4). This eliminates the need for variable argument lists (… arguments) in many (most?) places.
Which appears to me to support using intializer lists as general function arguments.
Anyway, regardless of the original intent of the design of a language feature I don't think it is especially significant to discussions of appropriate uses to which it can be put once it is part of the language.
Templates are used for many things that were not part of the original motivation for introducing them but that doesn't mean all of those uses are automatically to be considered bad.
Now std::initializer_list is not a normal container type
and I think it rarely if ever makes sense as a local variable and I can't think of a good use for one as a member variable but if it is a good parameter type for a container constructor I don't see the logic for saying it is a bad parameter type for a function if the function intends to access the contents inside the body for some purpose other than container initialization and never reference the contents after the function returns. Semantically, syntactically and safety wise how are those uses different?
Now I do acknowledge that constructing an array_view from any temporary (initializer list or container) is dangerous but as I said in the discussion on the issue, array_view is a fundamentally unsafe class and it is already proposed that there be static analysis support to catch unsafe uses that cannot be prevented at the library API level. If we get static analysis that can catch this:
auto v = makeVector();auto av = array_view<int>{v};v.push_back(4);// oops, potential dangling referenceThen it should be relatively easy to catch this:auto av = array_view<int>{makeVector()};// oops, definite dangling referenceSo why allow the first and not the second?
In particular, writing code like this makes the push_back bug impossible:void f(array_view<int>);f(makeVector());Which is one of the reasons I think that is strictly better style thanauto v = makeVector();auto av = array_view<int>{v};f(av);v.push_back();// oops, better not reference av after this point
auto v = makeVector();
f(v);
v.push_back();
auto adapt(initializer_list<T> list) {return array_view<T>{list.begin(), list.size()};}
I think you're equating things that aren't equal.
Lifetime extension for temporaries bound to references is one thing. Temporaries are real objects, easily visible in the code. You can see that they're being bound to a temporary (or that binding is hidden in code transformation like range-based for). Overall, it's clear what's happening.
Initializer lists aren't like that. The object itself is invisible, its definition unknown and unseen. The object can not be manipulated or treated like a real object. You can only talk about it through a reference. And that reference doesn't even look like a reference.
When you do something wrong with references, like returning a reference to a local from a function... you have to actually return one. You need to put "&" in the return type, thus making it abundantly clear that you're returning a reference. And thus at least potentially doing something wrong.
Returning an `initializer_list<int>` looks exactly like returning a `vector<int>`. And yet, it is almost always undefined behavior.
What good does learning the rules of `initializer_list` do? What's the point of having parameters of type `initializer_list`?
It allows some syntactic sugar on the user's side.
That's it. It isn't a need. It isn't a case of "I can't really do this without it". It's merely nicer syntax. Slightly.
I'd rather people use what is obviously correct than have them memorize and follow a bunch of complex rules. Like "it's safe to use `initializer_list` in parameters and locals, but not in return values or NSDMs." Or whatever.
... Did you read the same paragraph that I did? Like the part about "eliminates the need for variable argument lists (… arguments) in many (most?) places?"
Because that totally did not happen. You do not see a rash of C++ programmers abandoning template parameter packs in favor of initializer lists. And really, why would they?
It should also be noted that rule #1 about member variables? Well, `array_view` has member variables. And its constructor initializes them. Which means that any constructor from a compatible initializer list would... have to effectively initialize the array view with the initializer list.
So you're basically asking to violate rule #1.
That's a rather silly question.
Whether that `push_back` operation causes a dangling reference depends on two factors both being true:
1) `av` is used after the `push_back`
2) The `push_back` caused reallocation
Unless reallocation actually happened, this code is fine. The user may well have reserved sufficient space. Or maybe the user checked the capacity before doing the push_back. In any case, local static analysis cannot possibly be sufficient to prove that this code is broken.
By contrast, in your second case any use of `av` after its creation is always undefined behavior and obviously so. Static analysis wouldn't take long to find that dangling reference.
But really, worse-comes-to-worse, you can just define a quickie function of your own:
auto adapt(initializer_list<T> list) {return array_view<T>{list.begin(), list.size()};}
Similar code could be written for adapting rvalue references.
template<typename C> array_view<std::add_const_t<typename C::value_type>> av(const C& x) { return {x}; }
On Sunday, 18 October 2015 19:36:55 UTC-7, Nicol Bolas wrote:I think you're equating things that aren't equal.I don't think I am. If I'm reading or reviewing some code I don't see any safety / correctness reason to consider any of these particularly suspicious:
When you do something wrong with references, like returning a reference to a local from a function... you have to actually return one. You need to put "&" in the return type, thus making it abundantly clear that you're returning a reference. And thus at least potentially doing something wrong.
Returning an `initializer_list<int>` looks exactly like returning a `vector<int>`. And yet, it is almost always undefined behavior.Nothing in the discussion so far has mentioned returning an initializer_list from a function. I'm talking about taking it as a parameter to a function / passing it as an argument to a function and using it directly as the initializer for a range based for.
What good does learning the rules of `initializer_list` do? What's the point of having parameters of type `initializer_list`?
It allows some syntactic sugar on the user's side.
That's it. It isn't a need. It isn't a case of "I can't really do this without it". It's merely nicer syntax. Slightly.I believe syntax is important. That belief presumably was a motivator for being able to finally writeconst vector<int> v{1, 2, 3};Rather than:vector<int> v;v.reserve(3);v.push_back(1);v.push_back(2);v.push_back(3);And as a direct consequence being able to write:void f(const vector<int>&);f({1, 2, 3});I'm very glad we got that 'merely nicer syntax'. The way we got it is by allowing parameters of type initializer_list to constructors. It was explicitly decided according to that quote to allow it for any function and again, I'm glad that was the decision.
I'd rather people use what is obviously correct than have them memorize and follow a bunch of complex rules. Like "it's safe to use `initializer_list` in parameters and locals, but not in return values or NSDMs." Or whatever.One rule: it's safe to use initializer_list as a parameter.
On Sunday, 18 October 2015 21:34:34 UTC-7, Nicol Bolas wrote:... Did you read the same paragraph that I did? Like the part about "eliminates the need for variable argument lists (… arguments) in many (most?) places?"
Because that totally did not happen. You do not see a rash of C++ programmers abandoning template parameter packs in favor of initializer lists. And really, why would they?As I said before, I'm not particularly interested in dissecting the motivations underlying the original design of the language feature. I'm more interested in figuring out what good style means for the language rules we've actually ended up with. The original intent can be helpful guidance for that but that's about as far as it goes.That said, I really don't think there's much overlap between the use cases for template parameter packs and initializer_list arguments.
Equally, if you have a function that expects a container of ints as an argument, accepting an initializer_list as well as other compatible containers
It should also be noted that rule #1 about member variables? Well, `array_view` has member variables. And its constructor initializes them. Which means that any constructor from a compatible initializer list would... have to effectively initialize the array view with the initializer list.
So you're basically asking to violate rule #1.That doesn't follow at all - array_view doesn't have a member variable of type initializer_list and I'm not suggesting it should. I'm suggesting it be possible to directly construct an array_view from an initializer_list.
As you point out below, it will still be possible to do that anyway using the array_view constructor that takes a pointer and a length.
That's a rather silly question.
Whether that `push_back` operation causes a dangling reference depends on two factors both being true:
1) `av` is used after the `push_back`
2) The `push_back` caused reallocation
Unless reallocation actually happened, this code is fine. The user may well have reserved sufficient space. Or maybe the user checked the capacity before doing the push_back. In any case, local static analysis cannot possibly be sufficient to prove that this code is broken.If I saw this code while reading code or doing a code review I'd be suspicious, and I'd argue anyone else should be too.
This is why I still claim that 1) is strictly better style than 2):void f(array_view<int>);// 1f(vector<int>{1, 2, 3})// 2auto v = vector<int>{1, 2, 3};auto av = array_view<int>{v};f(av);Because if you don't need av after the call to f(), I as a reader don't want to have to manually check you don't actually use it.
By contrast, in your second case any use of `av` after its creation is always undefined behavior and obviously so. Static analysis wouldn't take long to find that dangling reference.Which is why I don't consider it a major enough problem to outlaw the safe uses like f({1, 2, 3}). I made that point in the original issue: GSL is intended to work alongside static analysis tools that actually do try to catch the push_back() scenario. If they can catch that, then catching creation of an array_view from a dangling temporary seems like it should be relatively trivial. So why use a coarse facility like removing && constructors that as a side effect disable safe and convenient uses?
But really, worse-comes-to-worse, you can just define a quickie function of your own:
auto adapt(initializer_list<T> list) {return array_view<T>{list.begin(), list.size()};}
Similar code could be written for adapting rvalue references.Yes, I pointed out as much in the discussion of a related issue and I will if necessary. I'd just rather not have to. The fact that the wrapper code to work around the removal of && constructors of containers is a relatively trivial
template<typename C> array_view<std::add_const_t<typename C::value_type>> av(const C& x) { return {x}; }
Highlights to me that deleting the && constructors in the first place is a crude attempt to work against the spirit of the language which happily converts temporaries to const references
The two things you were equating was the lifetime extension rules of temporaries and the use of initializer_lists. That a good C++ programmer should rely on the former, so why not rely on the latter too.
Your examples don't address that point. I was explaining how they're not the same thing, since temporaries are visible, while the object storing the initializer_list is not.
If you normalize the use of initializer_list by encouraging them to be used as temporary arrays for any purposes, you will encourage people to think that plenty of other use cases are legal. Like returning them.
That's "merely nicer syntax" for initializing variables, not for using `initializer_lists` as shorthands for temporary arrays. The latter is what you are trying to do.
But that's not the rule you're talking about. You want to use `initializer_list` as a way to construct an array_view. That's rather different from being "a parameter", don't you think? The effect is to take a reference to the temporary initializer list.
Equally, if you have a function that expects a container of ints as an argument, accepting an initializer_list as well as other compatible containers
You keep proving exactly why we should forbid such things. Because you keep believing that `initializer_list` is a container. It is not. And treating it as though it were a container is very much wrong.
That doesn't follow at all - array_view doesn't have a member variable of type initializer_list and I'm not suggesting it should. I'm suggesting it be possible to directly construct an array_view from an initializer_list.
... what?
You do know that an `initializer_list` is just a pair of pointers, right? And that `array_view` is (or can be implemented as) a pair of pointers. And therefore, if an array_view is constructed from an `initializer_list`, then it is storing an `initializer_list` in its NSDMs.
Oh sure, `array_view` does not literally store an `initializer_list` member. But it is exactly equivalent to storing one, with all of the inherent dangers therein.
As you point out below, it will still be possible to do that anyway using the array_view constructor that takes a pointer and a length.
Yes. And the fact that it requires special syntax should clue readers in that something potentially dangerous is going on. That's why it should not be implicitly allowed.
Potentially unsafe or ill-advised things should not be implicitly allowed.
Suspicious, perhaps. But that's the thing about static analyzers. The last thing you want is a bunch of false positives, because eventually... you'll just start ignoring them.
It is perfectly valid, and quite frankly entirely reasonable, to rely on the ability to add more elements to a vector without invalidating iterators. Yes, you want to make sure that said code isn't broken. But static analysis alone can't resolve that.
This is why I still claim that 1) is strictly better style than 2):void f(array_view<int>);// 1f(vector<int>{1, 2, 3})// 2auto v = vector<int>{1, 2, 3};auto av = array_view<int>{v};f(av);Because if you don't need av after the call to f(), I as a reader don't want to have to manually check you don't actually use it.
Or, you could just do `f(v)` like most people. Your insistence on making the `array_view` a variable that persists after its use is a strawman argument: designed specifically to fail, yet it misrepresents the actual position.
Also, the fact that the language converts temporaries to `const&` doesn't mean that a type forbidding conversion from temporaries is somehow wrongly designed. Indeed, if the "spirit of the language" was what you claim it to be, the language would make it impossible to forbid such conversion. That it wouldn't let you recognize whether something is a temporary at all.
On Monday, 19 October 2015 06:39:09 UTC-7, Nicol Bolas wrote:
Can you give me an example of another situation where the simplifying assumption that ordinary C++ lifetime rules for temporaries apply if you view a braced init list as a 'visible temporary' would lead to writing bad code?
vector<int> func()
{
const vector<int> &le = ...;
//do stuff
return le;
}
initializer_list<int> func()
{
initializer_list<int> le = ...;
return le;
}
If you normalize the use of initializer_list by encouraging them to be used as temporary arrays for any purposes, you will encourage people to think that plenty of other use cases are legal. Like returning them.I still think that the straightforward rule that std::initializer_list is safe to use as a parameter and should be avoided elsewhere covers this.
I can understand where the concern comes from but it's a hypothetical concern and it doesn't worry me overly much to be honest. I don't find any argument I've seen so far persuasive that this is something I should actually be concerned about.
On Monday, October 19, 2015 at 2:09:07 PM UTC-4, Matt Newport wrote:Can you give me an example of another situation where the simplifying assumption that ordinary C++ lifetime rules for temporaries apply if you view a braced init list as a 'visible temporary' would lead to writing bad code?
This is perfectly legal code, based on temporary life extension rules and copy construction:
vector<int> func()
{
const vector<int> &le = ...;
//do stuff
return le;
}
This is based on the same rules:
initializer_list<int> func()
{
initializer_list<int> le = ...;
return le;
}
Yet it does not work.
A user who believes that `initializer_list` works the same way as temporaries for any type will expect both of these to work. Yet the latter does not. And if a user has to understand why #2 fails and #1 works, then they know too much about C++.
Here's the thing: You asked for the reasoning. We've provided it. You have agreed that there is a concern. You simply decide that it doesn't "worry me overly much."
Well, it does worry other people. And we don't believe that the few minor syntactic conveniences are worth encouraging users to use `initializer_list`.
I wouldn't be too sure.
Check out the current draft of the ranges proposal (pdf). In particular, appendix C.10, which says:
-> Algorithms that do not mutate their input should accept `initializer_list` arguments wherever `Iterable`s are allowed.
So you're not the only person who agrees with you.
The downside of this design is that it is sometimes desirable to do this:// Try to adapt an rvalue container
auto rng = vector<int>{1,2,3,4} | view::reverse; // OK?
Adaptors operate on and yield Ranges; other Iterables (i.e., containers) are used to construct Ranges by first taking their begin and end. The code above is unsafe becauserng
will be left holding invalid iterators into a container that no longer exists. Our solution is to disallow the above code. It is illegal to adapt an rvalue non-Range. (Adapting rvalue Ranges, however, is perfectly acceptable; indeed necessary if adaptor pipelines are to work.) See Appendix 6 for the mechanics of how we distinguish between Iterables and Ranges.
Actions
When you want to mutate a container in-place, or forward it through a chain of mutating operations, you can use actions. The following examples should make it clear.
Read data into a vector, sort it, and make it unique.
extern std::vector<int> read_data();
using namespace ranges;
std::vector<int> vi = read_data() | action::sort | action::unique;
std::vector<int> foo();
void bar(std::array_view<int>);
bar(foo()); // oops, bar receives a dangling reference!
On 2015–11–03, at 2:17 AM, Matthew Woehlke <mwoehlk...@gmail.com> wrote:Oh. Ugh... here's where it breaks down:
auto&& av = std::array_view<int>(foo());
// oops, av is invalid!
That's more subtle than I was recalling that the problem was. Which...
is perhaps even more scary.