Braced-init-lists were originally used in object initialization with aggregate initialization. In the development of C++11, the idea was extended to include non-aggregate types through the use of a special initializer_list type.
This idea was ultimately extended into what became known as uniform initialization: one could initially any type via the use of a braced-init-list. Doing so provides a number of advantages, among them is uniform initialization behavior and the elimination of the most vexing parse. In a perfect world, we would always use uniform initialization and never use direct constructor syntax.
However, despite being called “uniform initialization,” it cannot be uniformly used everywhere, due to a critical flaw that prevents its use towards these ends. This proposal intends to correct this flaw, thus allowing it to be used in all possible cases of object construction, while having well-defined results.
The flaw in uniform initialization is quite simple to see in the standard library. The std::vector type has an explicit constructor which takes a single std::vector::size_type, which is an integral type. std::vector also has a constructor that takes an initializer_list<T>.
For most types, these constructors can coexist with uniform initialization quite reasonably. For example:
std::vector<A> v1{20};
std::vector<A> v2{A{}};
std::vector<A> v3{A{}, A{}, A{}};In this example, v1 is an array of 20 value-constructed values.
v2 is an array of 1 value-constructed element.
v3 is an array of 3 value-constructed elements.
This is all well and good, until we do this:
std::vector<int> v1{20};
std::vector<int> v2{int{}};
std::vector<int> v3{int{}, int{}, int{}};v2 and v3 retain their old meaning. But
v1 is now very different. It is an array containing a single
element, the number 20. Why?
Because uniform initialization syntax always prefers initializer list constructors if
one is available that would fit the braced-init-list. This is a consequence of uniform
initialization syntax using the same syntax as initializer lists: the braced-init-list.
And since
std::vector<int>::vector(std::initializer_list<int>)
matches the braced-init-list ({20}), it will be preferred over
std::vector<int>::vector(std::vector<int>::size_type)
This is a problem because there is no way to get at the size_type constructor via uniform initialization. There is no syntax that we can employ which will cause the conflicting initializer_list constructor to be ignored or to do anything else that would allow us to get at a different set of constructors.
Now, this may seem like a rather unimportant issue. After all, if I know that I have to use constructor initialization with vector<int>, then I can simply do that. But consider code that is generic on the vector's type:
template<typename T>
std::vector<T> MakeVector()
{
return std::vector<T>{20};
}By all rights, this code should always do the same thing: return a vector
containing 20 value-initialized elements. But it does not.
MakeVector<int>() will return a vector
containing exactly one element.
This is of much greater concern when dealing with more intricate templates. Consider the following trivial template function:
template<typename T>
T Process()
{
T t{};
//do stuff with t;
return T{t};
}This function creates a temporary, does some processing, and returns a copy of it. This function requires that T be DefaultConstructible and CopyConstructible, in addition to whatever “do stuff with t” requires.
The problem is the last line. This line may violate the concept constraint. Why? Because there is no guarantee that T does not have an initializer_list constructor that can match with a T. For an example of such a class, consider std::vector<std::function<void()>> as our T.
The problem is that std::function has a non-explicit constructor that can
take any type that is CopyConstructible. And
std::vector<std::function> is CopyConstructible. Therefore, this
will call the initializer_list constructor. But the template function should not be
calling the initializer list constructor; it's not part of the allowed interface to T.
Therefore, Process violates the concept constraint, through no
fault on the user's part.
What this means is that you cannot use uniform initialization in generic code where the exact type of the object being constructed is derived from a template parameter. This is because there is no way for the user to explicitly choose which constructors to call.
The idea is quite simple. We will divide braced-init-list up into two variation: list-braced-init-list and constr-braced-init-list. Most of the text will remain the exact same, as they have almost identical behavior.
The difference is that 13.3.1.7's list initialization overload resolution rules will behave differently. list-braced-init-list initialization will work exactly as it does. constr-braced-init-list will work opposite to the current way. That is, it will check non-initializer_list constructors first, then go to the initializer_list ones if no matching constructors are found. All other uses will work identically; you can use these for aggregate initialization and so forth just as before.
The big issue is how constr-braced-init-lists are defined in the grammar. It should look like this:
{^ ... }The “{” token followed by the “^” token is what distinguishes the constr-braced-init-list from a list-braced-init-list.
I have no particular love for the use of “^” here. My reasons for its selection are listed in the Grammar section of the Design Discussions segment of the document.
Depending on how we want to handle the wording, we may need a new set of “list-initialization” types. 13.3.1.7 never mentions braced-init-list; it only operates in terms of list-initialization, which stems from a specific use of braced-init-list. We could have it say something like “if the braced-init-list that issues this constructor is a list-braced-init-list, then ...” Or we could provide different forms of “list-initialization.”
It should cause zero backwards compatibility problems. list-braced-init-lists should have the same behavior as before. Also, see the Design Decisions section, the Grammar part.
Finding the right grammar for this will likely be the biggest stumbling point. I would say that the most important metrics for a good piece of grammar are:
Backwards compatibility. Whatever grammar we use, it should be something that would have been illegal in C++11, no matter what literals, identifiers, and tokens come after it.
Brevity. It should be used a lot, so it shouldn't take up a lot of space.
This leaves few candidates. Here are some potential alternatives:
{ explicit ... } //Keyword makes it clear what's going on.
{, ...} //Comma at the start would normally be illegal.
{| ...}
We could in theory use any operator that only has a binary operation mode. The use of “^” could interfere with C++/CX and other similar library extensions, so “|” or some other operator would be acceptable.
The operator should probably be within the braces, as this makes it more explicit which is being used.
Originally, I considered a library-based solution to the issue. This would have required adding a special opaque type taken as a parameter to various calls. Since the opaque type is different from other types, it would prevent the braced-init-list from binding to an initializer_list constructor, thus disambiguating the call.
This is problematic for two reasons. First, it adds some complexity to various container classes, as they have to prevent the user from using the opaque type as the member or allocator types. But the primary reason that this is not a good solution is because it only solves the problem for the standard library.
We should not ask every user who writes an initializer_list constructor to go though and add a suffix parameter to various functions. This is a problem introduced by the language, and it is best solved in the language.
constr-braced-init-list, in the current design, is intended to work exactly like braced-init-list except for which constructors hide which. That is, it could still access initializer_list constructors.
We don't have to do that. There are two other alternatives. We could make constr-braced-init-list never use initializer list constructors. This would be more uniform in some respects:
vector<int> v1{20}; //Creates an array of 1 item.
vector<int> v2{^20}; //Creates an array of 20 items.
vector<int> v3{20, 30, 40}; //Creates an array of 3 items.
vector<int> v4{^20, 30, 40}; //Creates an array of 3 items.The oddball here is v2, which calls a constructor. It would be
more uniform to require this:
vector<int> v1{{^20}}; //Creates an array of 1 item.
vector<int> v2{^20}; //Creates an array of 20 items.
vector<int> v1{^{20, 30, 40}}; //Creates an array of 3 items.
vector<int> v1{^20, 30, 40}; //Compiler error; no appropriate constructor to call.The question then becomes whether constr-braced-init-list should retain the other similarities to regular braced-init-lists. Should this be allowed:
array<int, 3> a1{^10, 20, 30};That is, do we want to forbid constr-braced-init-lists from initializing with anything other than non-initializer_list constructors?
I would say no, and here is why.
Currently, std::allocator_traits::construct is defined in
terms of calling new(/*stuff*/)
T(std::forward<Args>(args)...). That will call constructors, but it
will only call constructors. If we have a simple struct that
could use aggregate initialization, we cannot use, for example,
emplace on it:
struct Agg {int first, int second};
std::vector<Agg> ag{{10, 20}, {20, 30}, {30, 40}}; //Aggregate initialization is OK here.
ag.emplace_back(10, 20); //Error: no aggregate initialization is possible.There is no reason why we shouldn't be able to do this. To have this, we would
need to be able to have std::allocator_traits::construct use uniform initialization
syntax: new(/*stuff*/) T{std::forward<Args>(args)...}.
However, this can hide constructors;
vector<vector<int>>::emplace_back(10) will add a vector
containing one element. This will break existing applications, not to mention make
it completely impossible to access the hidden constructors via
emplace.
By defining constr-braced-init-lists as being exactly equal to braced-init-list
except for which constructors it prefers, it makes it possible to redefine
std::allocator_traits::construct in terms of
constr-braced-init-lists. This now means that the
emplace calls can access aggregate initialization if the
type supports it.
There was an alternate design to resolve the issue. Simply declare that non-initializer_list constructors have priority, instead of the other way around. This would make these completely unambiguous:
std::vector<int> v1{20};
std::vector<int> v2{{20}};v1 is using the size_type constructor. v2 is clearly calling a single-argument constructor using a braced-init-list.
The obvious problem with this is that it is a breaking change and a silent one at that.
Even worse, it doesn't look uniform:
std::array<int, 6> a1 = {1, 2, 3, 4, 5, 6}; //Brace elision removes the extra pair.
std::vector<int> v1 = {1, 2, 3, 4, 5, 6}; //Brace "elision" possible.
std::array<int, 1> a2 = {1}; //Brace elision still works.
std::vector<int> v2 = {1}; //Brace "elision" no longer possible due to conflict. Does the wrong thing.Honestly, this would be the preferable solution, if it were not a breaking change. But it is, so we probably shouldn't do it.
Joel Lamotte
{^ ... }
Library vs Language
Originally, I considered a library-based solution to the issue. This would have required adding a special opaque type taken as a parameter to various calls. Since the opaque type is different from other types, it would prevent the braced-init-list from binding to an initializer_list constructor, thus disambiguating the call.
First, it adds some complexity to various container classes, as they have to prevent the user from using the opaque type as the member or allocator types.
--
On 30 December 2012 15:49, Nicol Bolas <jmck...@gmail.com> wrote:{^ ... }
Having yet another way to do initialization scares me; it adds a lot of complexity to the language overall, and I remain unconvinced it has enough bang for the buck.
It doesn't address all the problems people have now. Another "gotcha" is preferringtemplate<typename T>Foo::Foo(T&& foo)
overFoo::Foo(Foo const&)for non-const references to Foo objects.
Given our past experience, I have very little confidence that we will now solve the problem of uniform initialization once and for all.
Library vs Language
Originally, I considered a library-based solution to the issue. This would have required adding a special opaque type taken as a parameter to various calls. Since the opaque type is different from other types, it would prevent the braced-init-list from binding to an initializer_list constructor, thus disambiguating the call.
There are other uses for this in containers; namely, it would be nice if there was a syntax for default constructing elements in a container (as opposed to value constructing them).
First, it adds some complexity to various container classes, as they have to prevent the user from using the opaque type as the member or allocator types.
The way around that is to embed the type inside the container.
std::vector<int> v{1, std::vector<int>::value_init};
This idea was ultimately extended into what became known as uniform initialization: one could initially any type via the use of a braced-init-list. Doing so provides a number of advantages,
In a perfect world, we would always use uniform initialization and never use direct constructor syntax.
This is all well and good, until we do this:
std::vector<int> v1{20}; std::vector<int> v2{int{}}; std::vector<int> v3{int{}, int{}, int{}};
v2andv3retain their old meaning. Butv1is now very different. It is an array containing a single element, the number 20. Why?Because uniform initialization syntax always prefers initializer list constructors if one is available that would fit the braced-init-list. This is a consequence of uniform initialization syntax using the same syntax as initializer lists: the braced-init-list. And since
std::vector<int>::vector(std::initializer_list<int>)matches the braced-init-list ({20}), it will be preferred overstd::vector<int>::vector(std::vector<int>::size_type)This is a problem because there is no way to get at the size_type constructor via uniform initialization.
std::vector<int> v1{20};
instead of
std::vector<int> v(20);
in order to get 20 value-initialized elements?
There is no syntax that we can employ which will cause the conflicting initializer_list constructor to be ignored or to do anything else that would allow us to get at a different set of constructors.
std::vector<int> v(20);does exactly what we want.
Now, this may seem like a rather unimportant issue. After all, if I know that I have to use constructor initialization with vector<int>, then I can simply do that. But consider code that is generic on the vector's type:
template<typename T> std::vector<T> MakeVector() { return std::vector<T>{20}; }By all rights, this code should always do the same thing: return a vector containing 20 value-initialized elements.
This is of much greater concern when dealing with more intricate templates. Consider the following trivial template function:
template<typename T> T Process() { T t{}; //do stuff with t; return T{t}; }This function creates a temporary, does some processing, and returns a copy of it. This function requires that T be DefaultConstructible and CopyConstructible, in addition to whatever “do stuff with t” requires.
The difference is that 13.3.1.7's list initialization overload resolution rules will behave differently. list-braced-init-list initialization will work exactly as it does. constr-braced-init-list will work opposite to the current way. That is, it will check non-initializer_list constructors first, then go to the initializer_list ones if no matching constructors are found.
On Monday, December 31, 2012 1:49:21 AM UTC+4, Nicol Bolas wrote:This idea was ultimately extended into what became known as uniform initialization: one could initially any type via the use of a braced-init-list. Doing so provides a number of advantages,
Personally, I see only disadvantages. The initialization rules became too complex and hard to understand. In spite of that horrible complexity, we still don't have a lot of useful capabilities, that we might have if a more rational alternative design was accepted.
This is all well and good, until we do this:
std::vector<int> v1{20}; std::vector<int> v2{int{}}; std::vector<int> v3{int{}, int{}, int{}};
v2andv3retain their old meaning. Butv1is now very different. It is an array containing a single element, the number 20. Why?Because uniform initialization syntax always prefers initializer list constructors if one is available that would fit the braced-init-list. This is a consequence of uniform initialization syntax using the same syntax as initializer lists: the braced-init-list. And since
std::vector<int>::vector(std::initializer_list<int>)matches the braced-init-list ({20}), it will be preferred overstd::vector<int>::vector(std::vector<int>::size_type)This is a problem because there is no way to get at the size_type constructor via uniform initialization.
I see your "problem" from the other side: why should I ever want to usestd::vector<int> v1{20};
instead of
std::vector<int> v(20);
in order to get 20 value-initialized elements?
There is no syntax that we can employ which will cause the conflicting initializer_list constructor to be ignored or to do anything else that would allow us to get at a different set of constructors.
If we want to get 20 value-initialized elements, thenstd::vector<int> v(20);does exactly what we want.
Now, this may seem like a rather unimportant issue. After all, if I know that I have to use constructor initialization with vector<int>, then I can simply do that. But consider code that is generic on the vector's type:
template<typename T> std::vector<T> MakeVector() { return std::vector<T>{20}; }By all rights, this code should always do the same thing: return a vector containing 20 value-initialized elements.
I would definitely prefer other behavior: it should _always_ create a single element initialized with value 20 (unfortunately, this is not so according to the current rules).
return std::vector<T>{{20}};
This is of much greater concern when dealing with more intricate templates. Consider the following trivial template function:
template<typename T> T Process() { T t{}; //do stuff with t; return T{t}; }This function creates a temporary, does some processing, and returns a copy of it. This function requires that T be DefaultConstructible and CopyConstructible, in addition to whatever “do stuff with t” requires.
This code can be rewritten as follows:
template<typename T>
T Process()
{
T t{};
// do stuff with t;
return t;
}
The difference is that 13.3.1.7's list initialization overload resolution rules will behave differently. list-braced-init-list initialization will work exactly as it does. constr-braced-init-list will work opposite to the current way. That is, it will check non-initializer_list constructors first, then go to the initializer_list ones if no matching constructors are found.
What is the point in calling a constructor with multiple parameters by use of a braced-init-list?
std::string GetLuaString()
{
size_t len = 0;
const char *str = lua_tolstring(L, -1, &len);
{len, str}; //No need to type std::string here.
}Note that currently we are unable to
1) copy-initialize a base class subobject, or a non-static data member in a mem-initializer-list, or an object created by a new-expression or by a function-style type conversion,
2) direct-initialize objects returned by value from functions,
3) move items of the initializer-list,
4) handle items of unrelated types,
5) forward initializer-lists.
Alternative design might provide all these capabilities:
std::string s = "text";
auto &&li = { 1, std::move(s), "string" };
2) the lifetime of the temporary object designated by expression { 1, std::move(s), "string" } (call it braced-init-list object) is the same as the lifetime of the reference li (according to the usual rules for reference-initialization).
3) Expressions 1, std::move(s) and "string" are used to initialize references of types int &&, std::string && and const char (&)[7] respectively. The reference of type int && is bound to a temporary object whose lifetime is the same as the lifetime of the braced-init-list object.
4) Constructs
Type x{};
new Type{};
Type{};
perform value-initialization.
5) Constructs
Type x = y;
Type x{y};
new Type{y};
Type x{y};
perform copy-initialization.
It's too late to do right things now, though.
On Tuesday, January 1, 2013 8:55:34 AM UTC-8, Nikolay Ivchenkov wrote:On Monday, December 31, 2012 1:49:21 AM UTC+4, Nicol Bolas wrote:Personally, I see only disadvantages. The initialization rules became too complex and hard to understand. In spite of that horrible complexity, we still don't have a lot of useful capabilities, that we might have if a more rational alternative design was accepted.
But they're not complex; the rules are quite simple.
I see your "problem" from the other side: why should I ever want to usestd::vector<int> v1{20};
instead of
std::vector<int> v(20);
in order to get 20 value-initialized elements?
Because it always works the same way, regardless of whether 20 is a literal or `int()` (which causes the most vexing parse. Admittedly not useful, but I don't have to change my syntax just for that).
The point of which is to always use {}; just read Stroustrup's C++ tour on the website, where he advocates always using {}.
template<typename T> std::vector<T> MakeVector() { return std::vector<T>{20}; }By all rights, this code should always do the same thing: return a vector containing 20 value-initialized elements.
I would definitely prefer other behavior: it should _always_ create a single element initialized with value 20 (unfortunately, this is not so according to the current rules).
But if you want that behavior you can guarantee it by doing this:return std::vector<T>{{20}};
There's no way to do the opposite: guarantee that you're calling the constructor.
This is of much greater concern when dealing with more intricate templates. Consider the following trivial template function:
template<typename T> T Process() { T t{}; //do stuff with t; return T{t}; }This function creates a temporary, does some processing, and returns a copy of it. This function requires that T be DefaultConstructible and CopyConstructible, in addition to whatever “do stuff with t” requires.
What is the point in calling a constructor with multiple parameters by use of a braced-init-list?
This:std::string GetLuaString()
{
size_t len = 0;
const char *str = lua_tolstring(L, -1, &len);
{len, str}; //No need to type std::string here.
}
This is one of the nice things that uniform initialization gives is: it allows us to create things without pointlessly naming the type again.
Note that currently we are unable to
1) copy-initialize a base class subobject, or a non-static data member in a mem-initializer-list, or an object created by a new-expression or by a function-style type conversion,
Why would you ever want to copy-initialize any of those? Copy initialization is a functional subset of direct initialization. Anything you could do with copy initialization you can do with direct initialization.
Oh, and one of the purposes of uniform initialization is so that you don't have to know the difference between direct and copy initialization.
Granted, they reneged on that by making copy-list-initialization not use explicit constructors, but there's a point to that difference.
3) move items of the initializer-list,
Of course not. Those items may be statically allocated.
4) handle items of unrelated types,5) forward initializer-lists.
... What does that have to do with anything being discussed here?
What you want is something we already have. It's called a tuple.
2) the lifetime of the temporary object designated by expression { 1, std::move(s), "string" } (call it braced-init-list object) is the same as the lifetime of the reference li (according to the usual rules for reference-initialization).
Yeah, that doesn't work. If braced-init-list is an object, it's members cannot extend the lifetime of temporary parameters that are passed to the constructor of that object. The constructor's argument will bind to the temporary first, and thus the lifetime will be the lifetime of the constructor argument, not the object itself. So that can't work.
3) Expressions 1, std::move(s) and "string" are used to initialize references of types int &&, std::string && and const char (&)[7] respectively. The reference of type int && is bound to a temporary object whose lifetime is the same as the lifetime of the braced-init-list object.
5) Constructs
Type x = y;
Type x{y};
new Type{y};
Type x{y};
perform copy-initialization.
So basically, you're saying that list-initialization should have been defined in terms of copy initialization instead of direct initialization. That's horrible; why would we ever want that?
The entire point of "uniform initialization" is that it is uniformly used to initialize everything.
You cannot copy-initialize every object you would ever want to create. You're going to have to use direct initialization. So if you're going to create a uniform initialization syntax, in order for it to be used everywhere, it must use direct constructor initialization.
You're saying that the design itself, the feature, is fundamentally wrong-headed.
This is about making uniform initialization work more like it's trying to.
make_unique<...>(initializer_list<int>{10, 1});
OK. How would your suggestion help to write templates like make_unique? (let's forgot about the distinction between copy-initialization and direct-initialization for a while)
template <class T, class... Params>
std::unique_ptr<T> make_unique(Params &&... params)
{
return std::unique_ptr<T>(new T{^std::forward<Params>(params)...});
}
int main()
{
auto p = make_unique<std::vector<int>>(10, 1);
// would create a container with 10 elements initialized with value 1
}
What should I do in order to get a sequence of two values: 10 and 1?
Like this:make_unique<...>(initializer_list<int>{10, 1});
The only reason the `initializer_list` is there is because the template argument deduction rules don't allow {} to be deduced from a bare template type. And I'm not sure if it's a good idea to allow that.
On Thursday, January 3, 2013 3:17:11 PM UTC+4, Nicol Bolas wrote:OK. How would your suggestion help to write templates like make_unique? (let's forgot about the distinction between copy-initialization and direct-initialization for a while)
template <class T, class... Params>
std::unique_ptr<T> make_unique(Params &&... params)
{
return std::unique_ptr<T>(new T{^std::forward<Params>(params)...});
}
int main()
{
auto p = make_unique<std::vector<int>>(10, 1);
// would create a container with 10 elements initialized with value 1
}
What should I do in order to get a sequence of two values: 10 and 1?
Like this:make_unique<...>(initializer_list<int>{10, 1});
That's verbose. Initialization with parentheses and list-initialization can already be roughly combined in a similar way in a library tool:
http://liveworkspace.org/code/1TOPrl$0
template<typename Ret, typename T>
std::unique_ptr<Ret> make_unique(std::initailizer_list<T> list)
{
return {new Ret{list}};
}
A core language construct could have some advantages (such as improved compile-time diagnostics, brevity, flexibility, and reduced translation time) over such a library solution, but It would look like a half measure that actually does not solve the most important initialization-related issues (brevity + readability + diagnostics, efficiency). By the way, if it will be accepted, when should we stop? I can imagine people who would like to use ()-like initialization with checking for narrowing conversions. Should the language have something like (^arg_list)-wise syntax for that? Note that {^arg_list} is not a superior alternative, because it's too common. We already have an example of a multipurpose facility - C-style cast. It can perform the same conversions as static_cast, reinterpret_cast and const_cast. Very "uniform" thing, right? So why should we ever prefer static_cast, reinterpret_cast or const_cast? That's because they give us an ability to express our intention more precisely. It seems that this simple idea was completely forgotten when the concept of list-initialization was under development.
Initialization with parentheses and list-initialization can already be roughly combined in a similar way in a library tool:
http://liveworkspace.org/code/1TOPrl$0
Which... only works in that one instance.
And it specifically picks constructors over list-initialization, and therefore it works exactly like using the {:} syntax I'm suggesting.
You're talking about a very specific problem: template type deduction of initializer lists. That's not the problem under discussion for this proposal.
Also, you could much more easily resolve this by simply adding an overload for make_unique:template<typename Ret, typename T>
std::unique_ptr<Ret> make_unique(std::initailizer_list<T> list)
{
return {new Ret{list}};
}
This will allow template argument deduction to attempt to deduce a naked braced-init-list as an initializer_list, thus deducing the type of that initializer list. Which will then be passed to Ret's constructor as an argument.
On Saturday, January 5, 2013 12:27:02 AM UTC+4, Nicol Bolas wrote:Initialization with parentheses and list-initialization can already be roughly combined in a similar way in a library tool:
http://liveworkspace.org/code/1TOPrl$0
Which... only works in that one instance.
new-expressions can be used to create objects on a stack/automatic memory. Such objects will never be treated as temporary though.
And it specifically picks constructors over list-initialization, and therefore it works exactly like using the {:} syntax I'm suggesting.
According to your description, in your case an aggregate initialization may take precedence over a valid ()-wise direct-initialization with other meaning.
But it seems that that would happen in exotic cases. I could disallow every list-initialization that is not an aggregate initialization if std::is_aggregate would be available.
You're talking about a very specific problem: template type deduction of initializer lists. That's not the problem under discussion for this proposal.
A resolution of such a "very specific problem" may conflict with your proposal or (most likely) be a superior solution for actual problems that your proposal could solve. I don't think that it would be wise to concentrate our attention on a single issue and ignore others, because suggestions for their resolution may potentially interact with each other.
Also, you could much more easily resolve this by simply adding an overload for make_unique:template<typename Ret, typename T>
std::unique_ptr<Ret> make_unique(std::initailizer_list<T> list)
{
return {new Ret{list}};
}
This will allow template argument deduction to attempt to deduce a naked braced-init-list as an initializer_list, thus deducing the type of that initializer list. Which will then be passed to Ret's constructor as an argument.
Such a resolution is not scalable. What if I want to pass 2 or more arguments to the constructor?
struct X
{
X(int, std::initializer_list<int>);
};
I haven't read the whole thread. Sorry if some of this has been
covered elsewhere.
> By all rights, this code should always do the same thing: return a vector
> containing 20 value-initialized elements. But it does not. MakeVector<int>()
> will return a vector containing exactly one element.
>
> This is of much greater concern when dealing with more intricate templates.
> Consider the following trivial template function:
>
> template<typename T>
> T Process()
> {
> T t{};
> //do stuff with t;
> return T{t};
> }
>
> This function creates a temporary, does some processing, and returns a copy
> of it. This function requires that T be DefaultConstructible and
> CopyConstructible, in addition to whatever “do stuff with t” requires.
>
> The problem is the last line. This line may violate the concept constraint.
> Why? Because there is no guarantee that T does not have an initializer_list
> constructor that can match with a T. For an example of such a class,
> consider std::vector<std::function<void()>> as our T.
Good point; I wouldn't have guessed that vector{same_type_vector}
could (attempt to) construct a 1-element vector instead of copying the
vector. That said, it can be fixed by using T(t) when intentionally
calling a copy constructor.
> ...
> Currently, std::allocator_traits::construct is defined in terms of calling
> new(/*stuff*/) T(std::forward<Args>(args)...). That will call constructors,
> but it will only call constructors. If we have a simple struct that could
> use aggregate initialization, we cannot use, for example, emplace on it:
Arguably, it's also a bug that foo.emplace() can't append a
vector<int> containing just {20}, but that isn't fixed by your
proposal.
{<special identifier>: ... }std::vector<int>{l: 20};
return {c: params...}; //Assuming we return a T by value.
This idea was ultimately extended into what became known as uniform initialization: one could initialize any type via the use of a braced-init-list. Doing so provides a number of advantages, among them is uniform initialization behavior and the elimination of the most vexing parse. In a perfect world, we would always use uniform initialization and never use direct constructor syntax.
The idea is a bit complex, but not unreasonably so. We recognize that there are 4 possible ways that a user could want to use uniform initialization:
Call a non-initializer_list constructor.
Call an initializer_list constructor.
Call a non-initializer_list constructor where applicable; if none fit, call an initializer_list constructor.
Call an initializer_list constructor where applicable; if none fit, call a non-initializer_list constructor.
These variation only affect which constructors are considered during overload resolution; in every other respect, they work like a regular braced-init-list. So if they're initializing an aggregate, all of them will work as normal via aggregate initialization rules. The only thing that changes, outside of the grammar that will be discussed presently, is section 13.3.1.7's list initialization overload resolution rules. They will cover the four cases.
This means that there must be four separate types of braced-init-lists. They will all behave the same except for how they interact with 13.3.1.7.
The big issue is how these special braced-init-lists are defined in the grammar. It should look like this:
{<special identifier>: ... }The “<special identifer>” is like the
“override” and “final” special identifiers in classes.
There will be four, each corresponding to a different scheme:
“c”: Call a non-initializer_list
constructor.
“l”: Call an initializer_list
constructor.
“cl”: Call a non-initializer_list constructor
where applicable; if none fit, call an initializer_list constructor.
I am not entirely convinced that we need this one. I personally really like the idea of forcing you to choose which set of constructors a braced-init-list should call. But since we can't get rid of the old syntax, orthogonality requires that we also allow the opposite convention to work.
“lc”: Call an initializer_list constructor
where applicable; if none fit, call a non-initializer_list constructor.
Equivalent to “{}”
Depending on how we want to handle the wording, we may need a new set of “list-initialization” types. 13.3.1.7 never mentions braced-init-list; it only operates in terms of list-initialization, which stems from a specific use of braced-init-list. We could have it say something like “if the braced-init-list that issues this constructor is one of these types, then ...” Or we could provide different forms of “list-initialization.”
It should cause zero backwards compatibility problems. Regular braced-init-lists should have the same behavior as before, and the new grammar would not have been legal before. Also, see the Design Decisions section.
What follows are some alternatives and discarded designs, and the reasons for not choosing them.
Originally, I considered a library-based solution to the issue. This would have required adding a special opaque type taken as a parameter to various calls. Since the opaque type is different from other types, it would prevent the braced-init-list from binding to an initializer_list constructor, thus disambiguating the call.
This is problematic for two reasons. First, it adds some complexity to various container classes, as they have to prevent the user from using the opaque type as the member or allocator types. But the primary reason that this is not a good solution is because it only solves the problem for the standard library.
We should not ask every user who writes an initializer_list constructor to go though and add a suffix parameter to various functions that could interfere. This is a problem introduced by the language, and it is best solved in the language.
The original design was essentially a binary choice: you pick constructors first or initializer_list first.
This was revealed to be a bad idea, because sometimes you want something to only consider non-init constructors or only consider initializer_list constructors. The enumeration of the four possibilities is important, especially for template code.
The reason why we need “c” when we could just
use regular constructor syntax is to allow the initialization of aggregates. For
example, consider this aggregate:
struct Agg
{
int x;
float y;
};We can initialize this via aggregate initialization:
Agg agg{5, 32.3f};Thanks to std::vector's initializer_list constructor, we can even initialize arrays of them:
std::vector<Agg> aggs{{-403, 234.0f}, {22, -19.89f}, {0, 4.2f}};And we can push them back into a vector:
aggs.push_back({3, 92.4f});What we can't do is emplace them back:
aggs.emplace_back(3, 92.4f); //Not allowed.
We can only initialize an aggregate in a standard library class by copy, not directly. Why not? Because allocator_traits<A>::construct explicitly calls the constructor (if A::construct doesn't exist). And Agg doesn't have a constructor.
With the proposed feature, we can easily redefine
allocator_traits<A>::construct in terms of {c:
params...}. This will call constructors or
employ aggregate initialization on the argument.
That wouldn't be possible without the “c” syntax. We (probably) don't want to allow initializer_list initialization through allocator_traits<A>::construct, but we should allow aggregate initialization via it.
There was an alternate design to resolve the issue. Simply declare that non-initializer_list constructors have priority, instead of the other way around. This would make these completely unambiguous:
std::vector<int> v1{20};
std::vector<int> v2{{20}};v1 is using the size_type constructor. v2 is clearly calling a single-argument constructor using a braced-init-list.
The obvious problem with this is that it is a breaking change and a silent one at that.
Even worse, it doesn't look uniform:
std::array<int, 6> a1 = {1, 2, 3, 4, 5, 6}; //Brace elision removes the extra pair.
std::vector<int> v1 = {1, 2, 3, 4, 5, 6}; //Brace "elision" possible.
std::array<int, 1> a2 = {1}; //Brace elision still works.
std::vector<int> v2 = {1}; //Brace "elision" no longer possible due to conflict. Does the wrong thing.Honestly, this would be the preferable solution, if it were not a breaking change. But it is, so we probably shouldn't do it.
There is a way to use this to make a noisy breaking change. If we effectively make {} equivalent to {“l”}, disallowing non-initializer_list constructors, then the compiler will at least complain about the problem. This would force the user to go through and annotate their expectations directly. And we have more uniformity: {} means initialize-from-list, while {:} means possibly call constructors.
Presently, explicit constructors cannot be called in copy-list-initialization contexts. This can force the use of a long, complex type in places where it is not needed. For example:
LongComplexFunctionReturnType<ThatIShouldn,tTypeAgain> FuncName(...)
{
//Compute stuff.
return LongComplexFunctionReturnType<ThatIShouldn,tTypeAgain>{params};
}The proposed syntax of providing parameters to the braced-init-list means that we could have additional parameters that allow a braced-init-list to call explicit constructors even in copy-list-initialization contexts:
LongComplexFunctionReturnType<ThatIShouldn,tTypeAgain> FuncName(...)
{
//Compute stuff.
return {explicit c: params};
}The main reason for introducing the copy-list-initialization to begin with was due
to concerns that not having it effectively made explicit
pointless, since people could easily end-run around it. This allows us to not have
to repeat the type, but we do have to at least state in our code the intent to call
explicit constructors.
Malte Skarupke: For bringing up the issue of using uniform initialization in generic code.
Joël Lamotte: For coming up with the idea to use “{:}” syntax.
Jeffrey Yasskin and Nikolay Ivchenkov, who offered valuable criticism of the idea that led to the current identifier syntax.
According to your description, in your case an aggregate initialization may take precedence over a valid ()-wise direct-initialization with other meaning.
When? If an object has a constructor, then it is not an aggregate.
You're talking about a very specific problem: template type deduction of initializer lists. That's not the problem under discussion for this proposal.
A resolution of such a "very specific problem" may conflict with your proposal or (most likely) be a superior solution for actual problems that your proposal could solve. I don't think that it would be wise to concentrate our attention on a single issue and ignore others, because suggestions for their resolution may potentially interact with each other.
There are only two possible solutions to this:
1: Define that a braced-init-list is deduced in a template as an initializer_list<T>, for some deduced type T based on some arbitrary rules.
2: Define that a braced-init-list is deduced as some completely new object with rules and behavior which is somehow magically able to be converted into perfectly-forwarded braced-init-list construction parameters on-demand.
There are really four different scenarios that you might come across with uniform initialization with regard to mapping a braced-init-list to a constructor. You might want:Each of these are very reasonable things for the user to want, and we should not force the user to have to live with one of these.
- Initializer-list constructors only.
- Non-init-list constructors only.
- Initializer-list, then others (the current behavior).
- Non-init-list constructors, then init-list constructors.
Therefore, if I want to create std::vector<int> with one value, I do this:std::vector<int>{l: 20};
And if I have some type T that I want to construct with a variadic parameter list, and I want T to be able to be an aggregate, but not to allow the parameter list to use initializer_list constructors:return {c: params...}; //Assuming we return a T by value.
On Saturday, January 5, 2013 3:43:54 AM UTC+4, Nicol Bolas wrote:
You're talking about a very specific problem: template type deduction of initializer lists. That's not the problem under discussion for this proposal.
A resolution of such a "very specific problem" may conflict with your proposal or (most likely) be a superior solution for actual problems that your proposal could solve. I don't think that it would be wise to concentrate our attention on a single issue and ignore others, because suggestions for their resolution may potentially interact with each other.
There are only two possible solutions to this:
1: Define that a braced-init-list is deduced in a template as an initializer_list<T>, for some deduced type T based on some arbitrary rules.
2: Define that a braced-init-list is deduced as some completely new object with rules and behavior which is somehow magically able to be converted into perfectly-forwarded braced-init-list construction parameters on-demand.
In the second case we could use ()-wise syntax and pass initializer lists as ordinary arguments.
Note that function templates like emplace_back would immediately get an ability to perform an aggregate initialization and forward initializer lists to initializer-list constructors, while library implementors, that currently use ()-wise initialization, would not need to change anything in their code for that. This would actually imply that ()-wise initialization would be a functionally uniform initialization with a single consistent semantics (as opposed to visually uniform {}-wise initialization with irregular dual semantics). Isn't this a superior alternative for your proposal?
On Saturday, January 5, 2013 7:31:23 AM UTC+4, Nicol Bolas wrote:There are really four different scenarios that you might come across with uniform initialization with regard to mapping a braced-init-list to a constructor. You might want:Each of these are very reasonable things for the user to want, and we should not force the user to have to live with one of these.
- Initializer-list constructors only.
- Non-init-list constructors only.
- Initializer-list, then others (the current behavior).
- Non-init-list constructors, then init-list constructors.
All of these 4 goals are not primary. Tasks #3 and #4 are purely contrived.
I know only two basic useful goals:
1) we want to perform an initialization wih a certain semantics (i.e. call a constructor with a certainly defined semantics among an overloaded set of constructors or perform an aggregate initialization and nothing else, etc.) [Examples: a) we want to create a vector<int> object with 20 value-initialized elements, b) we want to move an object];
2) we want to perform an initialization inside a library component and let a user of the library component to decide how exactly the initialization shall be done [Examples: a) std::make_shared, b) a container's emplace_back method].
For the former goal we should be able to express our intention clearly enough. For the latter goal a user should be able to specify desirable initialization semantics by means of an argument list for a function call, and the encoding should be concise and clear enough.
Therefore, if I want to create std::vector<int> with one value, I do this:std::vector<int>{l: 20};
That could help with the basic goal #1.
And if I have some type T that I want to construct with a variadic parameter list, and I want T to be able to be an aggregate, but not to allow the parameter list to use initializer_list constructors:return {c: params...}; //Assuming we return a T by value.
That doesn't look much helpful. In general, whether the object shall be initialized by an initializer-list constructor should be specified by a user of the template, not by the template definition. When it would be reasonable to do otherwise?
On Saturday, January 5, 2013 5:52:03 AM UTC-8, Nikolay Ivchenkov wrote:On Saturday, January 5, 2013 3:43:54 AM UTC+4, Nicol Bolas wrote:2: Define that a braced-init-list is deduced as some completely new object with rules and behavior which is somehow magically able to be converted into perfectly-forwarded braced-init-list construction parameters on-demand.
In the second case we could use ()-wise syntax and pass initializer lists as ordinary arguments.
Except we can't do that, because () means "expression". You can't just give () arbitrary semantics.
Note that function templates like emplace_back would immediately get an ability to perform an aggregate initialization and forward initializer lists to initializer-list constructors, while library implementors, that currently use ()-wise initialization, would not need to change anything in their code for that. This would actually imply that ()-wise initialization would be a functionally uniform initialization with a single consistent semantics (as opposed to visually uniform {}-wise initialization with irregular dual semantics). Isn't this a superior alternative for your proposal?
No. Besides the syntactic reality that it wouldn't work, it would also break various code because you then have to resolve the ambiguity between initializer_list constructors and non-initializer_list constructors.
There are really four different scenarios that you might come across with uniform initialization with regard to mapping a braced-init-list to a constructor. You might want:Each of these are very reasonable things for the user to want, and we should not force the user to have to live with one of these.
- Initializer-list constructors only.
- Non-init-list constructors only.
- Initializer-list, then others (the current behavior).
- Non-init-list constructors, then init-list constructors.
All of these 4 goals are not primary. Tasks #3 and #4 are purely contrived.
Considering that #3 is the current behavior, I suspect that others think differently about how "contrived" it is.
I know only two basic useful goals:
1) we want to perform an initialization wih a certain semantics (i.e. call a constructor with a certainly defined semantics among an overloaded set of constructors or perform an aggregate initialization and nothing else, etc.) [Examples: a) we want to create a vector<int> object with 20 value-initialized elements, b) we want to move an object];
And those "certain defined semantics" come in 4 flavors.
2) we want to perform an initialization inside a library component and let a user of the library component to decide how exactly the initialization shall be done [Examples: a) std::make_shared, b) a container's emplace_back method].
#2 is out of scope for this discussion. How some other code performs initialization is not the question. This is about when you, the writer of some code, are trying to initialize some object, and you want to do so along certain semantics. Forwarding initialization is a separate problem.
It's also a rather dubious request. We don't have ways to control how an object gets used externally in any other way.
If you call std::vector<T>::push_back, the type T must have a copy constructor. And so forth. You can't call push_back without that and expect it to work.
In every other case, the template itself puts restrictions on the interface for the types of its arguments. It defines exactly what your type needs to implement. Why should the user suddenly and magically have control over this for initialization when in any other scenario they would have to defer to the template's definition?
If a template says, "I expect to be able to call {lc: ...} to initialize the type you pass for some set of values"
On Saturday, January 5, 2013 11:22:35 PM UTC+4, Nicol Bolas wrote:On Saturday, January 5, 2013 5:52:03 AM UTC-8, Nikolay Ivchenkov wrote:On Saturday, January 5, 2013 3:43:54 AM UTC+4, Nicol Bolas wrote:2: Define that a braced-init-list is deduced as some completely new object with rules and behavior which is somehow magically able to be converted into perfectly-forwarded braced-init-list construction parameters on-demand.
In the second case we could use ()-wise syntax and pass initializer lists as ordinary arguments.
Except we can't do that, because () means "expression". You can't just give () arbitrary semantics.
We already use ()-wise syntax for direct-initialization.
Note that function templates like emplace_back would immediately get an ability to perform an aggregate initialization and forward initializer lists to initializer-list constructors, while library implementors, that currently use ()-wise initialization, would not need to change anything in their code for that. This would actually imply that ()-wise initialization would be a functionally uniform initialization with a single consistent semantics (as opposed to visually uniform {}-wise initialization with irregular dual semantics). Isn't this a superior alternative for your proposal?
No. Besides the syntactic reality that it wouldn't work, it would also break various code because you then have to resolve the ambiguity between initializer_list constructors and non-initializer_list constructors.
The model with ~{items} shown above seems to be pretty unambiguous.
There are really four different scenarios that you might come across with uniform initialization with regard to mapping a braced-init-list to a constructor. You might want:Each of these are very reasonable things for the user to want, and we should not force the user to have to live with one of these.
- Initializer-list constructors only.
- Non-init-list constructors only.
- Initializer-list, then others (the current behavior).
- Non-init-list constructors, then init-list constructors.
All of these 4 goals are not primary. Tasks #3 and #4 are purely contrived.
Considering that #3 is the current behavior, I suspect that others think differently about how "contrived" it is.
Do you see the difference between the behavior of a tool and goals of a programmer?
I know only two basic useful goals:
1) we want to perform an initialization wih a certain semantics (i.e. call a constructor with a certainly defined semantics among an overloaded set of constructors or perform an aggregate initialization and nothing else, etc.) [Examples: a) we want to create a vector<int> object with 20 value-initialized elements, b) we want to move an object];
And those "certain defined semantics" come in 4 flavors.
There is nothing common between desirable semantics and the encoding rules that allow a compiler to determine the desirable interpretation of the program. A subset of considered constructors and the order in which they are considered are related to the encoding rules and they don't constitute any goals of ordinary programmers.
2) we want to perform an initialization inside a library component and let a user of the library component to decide how exactly the initialization shall be done [Examples: a) std::make_shared, b) a container's emplace_back method].
#2 is out of scope for this discussion. How some other code performs initialization is not the question. This is about when you, the writer of some code, are trying to initialize some object, and you want to do so along certain semantics. Forwarding initialization is a separate problem.
It's also a rather dubious request. We don't have ways to control how an object gets used externally in any other way.
std::make_shared and emplace_back just don't need to know that.
If you call std::vector<T>::push_back, the type T must have a copy constructor. And so forth. You can't call push_back without that and expect it to work.
In every other case, the template itself puts restrictions on the interface for the types of its arguments. It defines exactly what your type needs to implement. Why should the user suddenly and magically have control over this for initialization when in any other scenario they would have to defer to the template's definition?
I was talking about forwarding, because that your code sample looks like a case of forwarding. I could incorrectly interpret the intention, but that's because the code is too abstract. It would be interesting to see real world examples.If a template says, "I expect to be able to call {lc: ...} to initialize the type you pass for some set of values"
Usually requirements imposed on template arguments are not arbitrary, they reflect (ideally minimal) conditions under which the desirable semantics can be implemented. Could you provide an example of an underlying task for which "I expect to be able to call {lc: ...} to initialize the type you pass for some set of values" would be a reasonable condition?
We already use ()-wise syntax for direct-initialization.
Except for when it looks like a function definition.
Again, you seem to be ignoring the reasons why we want uniform initialization syntax.
Note that function templates like emplace_back would immediately get an ability to perform an aggregate initialization and forward initializer lists to initializer-list constructors, while library implementors, that currently use ()-wise initialization, would not need to change anything in their code for that. This would actually imply that ()-wise initialization would be a functionally uniform initialization with a single consistent semantics (as opposed to visually uniform {}-wise initialization with irregular dual semantics). Isn't this a superior alternative for your proposal?
No. Besides the syntactic reality that it wouldn't work, it would also break various code because you then have to resolve the ambiguity between initializer_list constructors and non-initializer_list constructors.
The model with ~{items} shown above seems to be pretty unambiguous.
First, the proposal has {l:}, which can have those semantics in template deduction situations.
More importantly, that doesn't solve the problem, because you don't provide a mechanism equivalent to {c:}.
Or to {cl:}, which is also desirable.
Do you see the difference between the behavior of a tool and goals of a programmer?
I don't see what you're getting at here.
The whole point of adding language features is to provide mechanisms for programmers to more directly express their intents and desires.
How the compiler decides which constructor to call is very much a part of the goal of any programmer, because it informs which tool the programmer can use
We don't have ways to control how an object gets used externally in any other way.
std::make_shared and emplace_back just don't need to know that.
But that's what you asked for: "user of the library component to decide how exactly the initialization shall be done" That's external (ie: not defined within the function) control over how an object gets used. So do you want external control over how a template type is initialized (ie: used) or do you not?
Usually requirements imposed on template arguments are not arbitrary, they reflect (ideally minimal) conditions under which the desirable semantics can be implemented. Could you provide an example of an underlying task for which "I expect to be able to call {lc: ...} to initialize the type you pass for some set of values" would be a reasonable condition?
emplace. make_shared. make_unique. Indeed, any function that forwards parameters for initialization purposes specifies as part of its concept how it wants to initialize the type. A good initialization forwarding function would use either {c:} (to force you to always use initializer_lists explicitly) or {cl:}, so that you could use aggregates with them.
On Sunday, January 6, 2013 3:02:01 AM UTC+4, Nicol Bolas wrote:Again, you seem to be ignoring the reasons why we want uniform initialization syntax.
I prefer to compare different approaches, rather than to concentrate on a single approach exclusively as if it would be the only possible solution.
Note that function templates like emplace_back would immediately get an ability to perform an aggregate initialization and forward initializer lists to initializer-list constructors, while library implementors, that currently use ()-wise initialization, would not need to change anything in their code for that. This would actually imply that ()-wise initialization would be a functionally uniform initialization with a single consistent semantics (as opposed to visually uniform {}-wise initialization with irregular dual semantics). Isn't this a superior alternative for your proposal?
No. Besides the syntactic reality that it wouldn't work, it would also break various code because you then have to resolve the ambiguity between initializer_list constructors and non-initializer_list constructors.
The model with ~{items} shown above seems to be pretty unambiguous.
First, the proposal has {l:}, which can have those semantics in template deduction situations.
Can {l:items} be forwarded?
If not, then ~{items} with ()-wise initialization syntax solves more issues than {l:items}:
1) disambiguation between arguments for constructor and elements of a tuple-like entity;
2) forwarding.
More importantly, that doesn't solve the problem, because you don't provide a mechanism equivalent to {c:}.
I'm not even sure what exactly {c:} is supposed to do.
struct X
{
X(int);
X(std::initializer_list<int>);
};
void f()
{
X x1{c: std::initializer_list<int>{20}}; // ?
X x2{c: {20}}; // ?
X x3{c: {c: 20}}; // ?
}
What should happen in these cases?
Should {c: items} prevent narrowing conversions?
void g(int n)
{
double d1{n}; // ill-formed according to the C++11 rules
double d2{c: n}; // is this well-formed or not?
}
Or to {cl:}, which is also desirable.
For what purposes?
Do you see the difference between the behavior of a tool and goals of a programmer?
I don't see what you're getting at here.
That explains the strange wishes.The whole point of adding language features is to provide mechanisms for programmers to more directly express their intents and desires.
How exactly would {cl:} and {lc:} help to express intents and desires more directly?
std::vector<int> v = {20, 30, 40, 50};
std::vector<std::vector<int>> vv;
vv.emplace_back(20, 30, 40, 50);
We don't have ways to control how an object gets used externally in any other way.
std::make_shared and emplace_back just don't need to know that.
But that's what you asked for: "user of the library component to decide how exactly the initialization shall be done" That's external (ie: not defined within the function) control over how an object gets used. So do you want external control over how a template type is initialized (ie: used) or do you not?
I was talking about internal control (within the template definition) over an external usage (on the user side).
Usually requirements imposed on template arguments are not arbitrary, they reflect (ideally minimal) conditions under which the desirable semantics can be implemented. Could you provide an example of an underlying task for which "I expect to be able to call {lc: ...} to initialize the type you pass for some set of values" would be a reasonable condition?
emplace. make_shared. make_unique. Indeed, any function that forwards parameters for initialization purposes specifies as part of its concept how it wants to initialize the type. A good initialization forwarding function would use either {c:} (to force you to always use initializer_lists explicitly) or {cl:}, so that you could use aggregates with them.
There can exist a superior approach for forwarding:
struct Aggregate { int x, y; };
void f()
{
auto p1 = make_unique<Aggregate>(~{2, 1});
// aggregate initialization: new Aggregate(~{2, 1})
auto p2 = make_unique<std::vector<int>>(2, 1);
// initialization by the non-initializer-list constructor: new std::vector<int>(2, 1)
auto p3 = make_unique<std::vector<int>>(~{2, 1});
// initialization by the initializer-list constructor: new std::vector<int>(~{2, 1})
}
struct Type { int x, y; Type(int _x, int _y) : x(_x), y(_y) };
auto p4 = make_unique<Type>(2, 1);
// initialization by the constructor.
auto p5 = make_unqiue<Type>(~{2, 1});
// Compiler error; cannot initialize from an initializer_list constructor.
Are there any other tasks for which {lc: ...} would be useful?
On Sunday, January 6, 2013 12:03:34 PM UTC-8, Nikolay Ivchenkov wrote:
I prefer to compare different approaches, rather than to concentrate on a single approach exclusively as if it would be the only possible solution.
That's wonderful; do it elsewhere.
Can {l:items} be forwarded?
What do you mean by "forwarded"?
Should {c: items} prevent narrowing conversions?void g(int n)
{
double d1{n}; // ill-formed according to the C++11 rules
double d2{c: n}; // is this well-formed or not?
}
I'll pretend that `n` is a double and `d1` and `d2` are ints, because otherwise there's no narrowing (int to double is not narrowing).
And yes, per the uniform initialization rules, narrowing would be forbidden.
You keep missing the simplicity of the proposal: the only thing that changes is which constructors get enumerated by 13.3.1.7's overloading rules and in what order. Nothing else about uniform initialization is affected.
How exactly would {cl:} and {lc:} help to express intents and desires more directly?
Because that's what some people want. Some people want these to work the same:std::vector<int> v = {20, 30, 40, 50};
std::vector<std::vector<int>> vv;
vv.emplace_back(20, 30, 40, 50);
vv.emplace_back(20, 30, 40, 50) is much less informative (for people who will read the code) than vv.emplace_back(~{20, 30, 40, 50}).For some people, that's a reasonable thing to want to be the same. If they pass a bunch of values, then they should be able to be formed into an initializer_list constructor. That is, "emplace_back" should use every tool available to actually construct the object from the parameters it is given.
It is not my place to second-guess the desires of a programmer. It's my place to allow them to effectively express those desires to the compiler.
You seem to have missed the non-uniformity:struct Type { int x, y; Type(int _x, int _y) : x(_x), y(_y) };
auto p4 = make_unique<Type>(2, 1);
// initialization by the constructor.
auto p5 = make_unqiue<Type>(~{2, 1});
// Compiler error; cannot initialize from an initializer_list constructor.
Why should `Type` be initialized via direct constructor initialization, while `Aggregate` has to use your `~{}` syntax?
That's not uniform
and therefore works against the whole point of uniform initialization.
I, as the user, should neither know nor care if `Aggregate` or `Type` has a constructor or is an aggregate.
Also, your code doesn't allow aggregate initialization in cases where `Aggregate` isn't just a bunch of `ints`.
Also, forwarding is not an initialization problem; it's a separate issue. You cannot forward the mechanics of braced-init-lists, because braced-init-lists are not expressions or values.
Are there any other tasks for which {lc: ...} would be useful?
I don't second-guess Bjarne Stroustrup. {lc:} is the current behavior, and he clearly thought it was reasonable for some reason.
On Monday, January 7, 2013 1:30:39 AM UTC+4, Nicol Bolas wrote:On Sunday, January 6, 2013 12:03:34 PM UTC-8, Nikolay Ivchenkov wrote:
I prefer to compare different approaches, rather than to concentrate on a single approach exclusively as if it would be the only possible solution.
That's wonderful; do it elsewhere.
I could write an alternative proposal, but it's not likely that it would be approved. As well as yours.
Can {l:items} be forwarded?
What do you mean by "forwarded"?
I mean the ability to write a function template f, that creates an object x of type T so that for every well-defined {l:items} or for the vast majority of such constructs the call f<T>({l:items}) would perform initialization of x in the same way as the variable definition of the form T x({l:items}).
And yes, per the uniform initialization rules, narrowing would be forbidden.
You keep missing the simplicity of the proposal: the only thing that changes is which constructors get enumerated by 13.3.1.7's overloading rules and in what order. Nothing else about uniform initialization is affected.
You are suggesting to make a breaking change then: if emplace_back would use your syntax, then the following code would not be valid anymore.
std::vector<double> v;
v.emplace_back(0); // error: narrowing conversion from int to double
How exactly would {cl:} and {lc:} help to express intents and desires more directly?
Because that's what some people want. Some people want these to work the same:std::vector<int> v = {20, 30, 40, 50};
std::vector<std::vector<int>> vv;
vv.emplace_back(20, 30, 40, 50);
Sometimes people want strange things. There should be an objective reason to do so. From my point of view,vv.emplace_back(20, 30, 40, 50)is much less informative (for people who will read the code) thanvv.emplace_back(~{20, 30, 40, 50}).
You seem to have missed the non-uniformity:struct Type { int x, y; Type(int _x, int _y) : x(_x), y(_y) };
auto p4 = make_unique<Type>(2, 1);
// initialization by the constructor.
auto p5 = make_unqiue<Type>(~{2, 1});
// Compiler error; cannot initialize from an initializer_list constructor.
Why should `Type` be initialized via direct constructor initialization, while `Aggregate` has to use your `~{}` syntax?
That's because an aggregate (as well as a container like std::vector<int>) can always be considered as a sequence of items (which can be initialized by the respective items of an initializer-list),
while a constructor with two parameters doesn't necessarily perform an itemwise initialization of some sequence. If we want to support an itemwise initialization, we could express this intent explicitly by defining a special constructor:
struct Type
{
int x, y;
Type(int _x, int _y) : // (1)
Type(~{_x, _y}) {}
Type(std::braced_init_list<int const &, int const &> &&li) : // (2)
_x(li.get<0>()), _y(li.get<1>()) {}
};
auto p = make_unqiue<Type>(~{2, 1}); // OK: calls (2)
That's not uniform
That's intentionally non-uniform.
Are there any other tasks for which {lc: ...} would be useful?
I was going to ask about {cl: ...}, but I made a typo :-)
I don't second-guess Bjarne Stroustrup. {lc:} is the current behavior, and he clearly thought it was reasonable for some reason.
I have a simple explanation of that current behavior: the items of the paragraph 8.5.4/3 are ordered in a way that for every particular case of application of list-initialization give us the behavior which would be the most likely expected (in opinion of the authors of the rules) among possible alternatives. In spite of that, list-initialization (being used solely) does not give us an ability to express every desirable way of initialization and often we get an unexpected behavior.
What follows is a draft of a proposal for adding new rules for uniform initialization syntax. It covers what it wants pretty well, so I'll let it speak for itself.
This idea was ultimately extended into what became known as uniform initialization: one could initially any type via the use of a braced-init-list. Doing so provides a number of advantages, among them is uniform initialization behavior and the elimination of the most vexing parse. In a perfect world, we would always use uniform initialization and never use direct constructor syntax.
The difference is that 13.3.1.7's list initialization overload resolution rules will behave differently. list-braced-init-list initialization will work exactly as it does. constr-braced-init-list will work opposite to the current way. That is, it will check non-initializer_list constructors first, then go to the initializer_list ones if no matching constructors are found. All other uses will work identically; you can use these for aggregate initialization and so forth just as before.
The big issue is how constr-braced-init-lists are defined in the grammar. It should look like this:
{^ ... }The “{” token followed by the “^” token is what distinguishes the constr-braced-init-list from a list-braced-init-list.
Note
I have no particular love for the use of “^” here. My reasons for its selection are listed in the Grammar section of the Design Discussions segment of the document.
Depending on how we want to handle the wording, we may need a new set of “list-initialization” types. 13.3.1.7 never mentions braced-init-list; it only operates in terms of list-initialization, which stems from a specific use of braced-init-list. We could have it say something like “if the braced-init-list that issues this constructor is a list-braced-init-list, then ...” Or we could provide different forms of “list-initialization.”
It should cause zero backwards compatibility problems. list-braced-init-lists should have the same behavior as before. Also, see the Design Decisions section, the Grammar part.
Finding the right grammar for this will likely be the biggest stumbling point. I would say that the most important metrics for a good piece of grammar are:
Backwards compatibility. Whatever grammar we use, it should be something that would have been illegal in C++11, no matter what literals, identifiers, and tokens come after it.
Brevity. It should be used a lot, so it shouldn't take up a lot of space.
This leaves few candidates. Here are some potential alternatives:
{ explicit ... } //Keyword makes it clear what's going on. {, ...} //Comma at the start would normally be illegal. {| ...}We could in theory use any operator that only has a binary operation mode. The use of “^” could interfere with C++/CX and other similar library extensions, so “|” or some other operator would be acceptable.
The operator should probably be within the braces, as this makes it more explicit which is being used.
Originally, I considered a library-based solution to the issue. This would have required adding a special opaque type taken as a parameter to various calls. Since the opaque type is different from other types, it would prevent the braced-init-list from binding to an initializer_list constructor, thus disambiguating the call.
This is problematic for two reasons. First, it adds some complexity to various container classes, as they have to prevent the user from using the opaque type as the member or allocator types. But the primary reason that this is not a good solution is because it only solves the problem for the standard library.
We should not ask every user who writes an initializer_list constructor to go though and add a suffix parameter to various functions. This is a problem introduced by the language, and it is best solved in the language.
constr-braced-init-list, in the current design, is intended to work exactly like braced-init-list except for which constructors hide which. That is, it could still access initializer_list constructors.
We don't have to do that. There are two other alternatives. We could make constr-braced-init-list never use initializer list constructors. This would be more uniform in some respects:
vector<int> v1{20}; //Creates an array of 1 item. vector<int> v2{^20}; //Creates an array of 20 items. vector<int> v3{20, 30, 40}; //Creates an array of 3 items. vector<int> v4{^20, 30, 40}; //Creates an array of 3 items.The oddball here is
v2, which calls a constructor. It would be more uniform to require this:vector<int> v1{{^20}}; //Creates an array of 1 item. vector<int> v2{^20}; //Creates an array of 20 items. vector<int> v1{^{20, 30, 40}}; //Creates an array of 3 items. vector<int> v1{^20, 30, 40}; //Compiler error; no appropriate constructor to call.The question then becomes whether constr-braced-init-list should retain the other similarities to regular braced-init-lists. Should this be allowed:
array<int, 3> a1{^10, 20, 30};That is, do we want to forbid constr-braced-init-lists from initializing with anything other than non-initializer_list constructors?
I would say no, and here is why.
Currently,
std::allocator_traits::constructis defined in terms of callingnew(/*stuff*/) T(std::forward<Args>(args)...). That will call constructors, but it will only call constructors. If we have a simple struct that could use aggregate initialization, we cannot use, for example,emplaceon it:
struct Agg {int first, int second}; std::vector<Agg> ag{{10, 20}, {20, 30}, {30, 40}}; //Aggregate initialization is OK here. ag.emplace_back(10, 20); //Error: no aggregate initialization is possible.There is no reason why we shouldn't be able to do this. To have this, we would need to be able to have std::allocator_traits::construct use uniform initialization syntax:
new(/*stuff*/) T{std::forward<Args>(args)...}. However, this can hide constructors;vector<vector<int>>::emplace_back(10)will add a vector containing one element. This will break existing applications, not to mention make it completely impossible to access the hidden constructors viaemplace.By defining constr-braced-init-lists as being exactly equal to braced-init-list except for which constructors it prefers, it makes it possible to redefine
std::allocator_traits::constructin terms ofconstr-braced-init-lists. This now means that theemplacecalls can access aggregate initialization if the type supports it.
There was an alternate design to resolve the issue. Simply declare that non-initializer_list constructors have priority, instead of the other way around. This would make these completely unambiguous:
std::vector<int> v1{20}; std::vector<int> v2{{20}};v1 is using the size_type constructor. v2 is clearly calling a single-argument constructor using a braced-init-list.
The obvious problem with this is that it is a breaking change and a silent one at that.
Even worse, it doesn't look uniform:
std::array<int, 6> a1 = {1, 2, 3, 4, 5, 6}; //Brace elision removes the extra pair. std::vector<int> v1 = {1, 2, 3, 4, 5, 6}; //Brace "elision" possible. std::array<int, 1> a2 = {1}; //Brace elision still works. std::vector<int> v2 = {1}; //Brace "elision" no longer possible due to conflict. Does the wrong thing.Honestly, this would be the preferable solution, if it were not a breaking change. But it is, so we probably shouldn't do it.
--
vector<int> v1{20}; //Creates an array of 1 item.
vector<Type> v1{20}; //Creates an array 20 Types.
vector<int> v1{l: 20}; //Creates an array of 1 item.
vector<Type> v1{l: 20}; //Compiler error; no matching initializer_list constructor
If you used {l:} instead:vector<int> v1{l: 20}; //Creates an array of 1 item.
vector<Type> v1{l: 20}; //Compiler error; no matching initializer_list constructor
Then you get a proper compiler error. The person doing the initialization has the ability to explicitly state what they want to call and what they don't.
Also, how does that work with aggregate initialization? Does it cause the initialization of a particular named field of the aggregate?
Plus, there's the general issue with named parameters at all, which is a very foreign concept to C++ in general.
The idea of a template's concept requiring a parameter name is very... disconcerting.
If you used {l:} instead:vector<int> v1{l: 20}; //Creates an array of 1 item.
vector<Type> v1{l: 20}; //Compiler error; no matching initializer_list constructor
What is supposed to be "l:" ?Is the name of a constructor parameter ?
Then you get a proper compiler error. The person doing the initialization has the ability to explicitly state what they want to call and what they don't.I don't understand what the point of this example.
Also, how does that work with aggregate initialization? Does it cause the initialization of a particular named field of the aggregate?
I think it does not apply in this case.
How does your proposal work with aggregate initialization?
Plus, there's the general issue with named parameters at all, which is a very foreign concept to C++ in general."foreign concept to C++" not disqualify its usefulness.It is a common concept, like many other additions to C++ that have come from other languages.
std::vector<Type> v1{20}; //Creates a vector of 20 default-constructed items.
> Design Overview
>
> The idea is quite simple. We will divide braced-init-list up into two
> variation: list-braced-init-list and constr-braced-init-list. Most of
> the text will remain the exact same, as they have almost identical
> behavior.
As far as I am concerned it is not clear what problem you are trying to
solve. Sorry, I don't consider "I can't use curly braces for
everything" to be a problem.
I don't like it. It does not seem to solve any problems but only cause
more. For example, this is not "perfectly forwardable".
If you intent to write your own types and like to be able to
distinguish between different constructors, consider tag dispatching
(like std::piecewise for std::pair).
If the above compiles, then clearly the design of the feature is to be usable for more than just aggregate and list initialization. It's there to solve the Most Vexing Parse issue
and be usable in all cases of initialization.
Therefore, if the feature can't be used everywhere, then it's not living up to its intended purpose: to universally replace all initialization. The purpose of this feature is to make uniform initialization usable in all contexts, so that C++ programmers don't have to go "well, sometimes we use {} to initialize, but you can't trust it so most of the time you use (), except when your compiler thinks it's a function so you use {} again, but not when..."
- "l": Only look at initializer_list constructors.
- "c": Only look at non-initializer_list constructors.
- "lc": Look at initializer_list constructors first. Equivalent to {}.
- "cl": Look at non-initializer_list constructors first.
It makes the language simpler to use.
{} is not "perfectly forwardable" as it currently stands either. A braced-init-list cannot be used in a parameter pack or any other template deduction contexts.
It's a problem before this proposal and it remains one after. It's not a problem this proposal is intended to solve.
On Tue, Jan 8, 2013 at 10:35 AM, Nicol Bolas wrote:
> If the above compiles, then clearly the design of the feature is to be
> usable for more than just aggregate and list initialization. It's there to
> solve the Most Vexing Parse issue and be usable in all cases of
> initialization.
> Therefore, if the feature can't be used everywhere, then it's not living up
> to its intended purpose: to universally replace all initialization.
But it is _not_ designed to be the syntax for all cases.
A fundamental aim of “uniform initialization” can be expressed as: “We want a single syntax that can be used everywhere and wherever it’s used, the same initialized value results for the same initializer value”. It may be that the ideal can be only approximated for C++0x, but I argue that it is the ideal.
On 8 January 2013 03:35, Nicol Bolas <jmck...@gmail.com> wrote:
If the above compiles, then clearly the design of the feature is to be usable for more than just aggregate and list initialization. It's there to solve the Most Vexing Parse issue
How does {} not solve the most vexing parse issue?
And at the call site, I don't see how anyone can mix up passing no parameters to a constructor vs. passing at least one parameter to a constructor.
and be usable in all cases of initialization.
Therefore, if the feature can't be used everywhere, then it's not living up to its intended purpose: to universally replace all initialization. The purpose of this feature is to make uniform initialization usable in all contexts, so that C++ programmers don't have to go "well, sometimes we use {} to initialize, but you can't trust it so most of the time you use (), except when your compiler thinks it's a function so you use {} again, but not when..."
And you want to add *three more* ways and *four more* cryptic syntaxes to do initialization:
- "l": Only look at initializer_list constructors.
- "c": Only look at non-initializer_list constructors.
- "lc": Look at initializer_list constructors first. Equivalent to {}.
- "cl": Look at non-initializer_list constructors first.
It makes the language simpler to use.
Given that most people read code far more often than they write it, I just don't see how having to learn the subtleties of *seven* (the seventh being value initialization vs. default initialization) different syntaxes is simplier than learning just three.
"Uniform" is supposed to mean *one*, not *seven*. I agree that the standard doesn't quite achieve it, but this proposal doesn't either.
{} is not "perfectly forwardable" as it currently stands either. A braced-init-list cannot be used in a parameter pack or any other template deduction contexts.
It's a problem before this proposal and it remains one after. It's not a problem this proposal is intended to solve.
So we'll need even *more* syntaxes to solve other related issues? What happened to simpler?
std::vector<int> v1{20}; //Doesn't call an initializer_list constructor.
std::vector<int> v2{20, 30, 40}; //Compiler error. No constructor takes 3 ints
std::vector<int> v3{{20, 30, 40}}; //Makes a list
On Tuesday, January 8, 2013 7:57:16 AM UTC-8, Nevin ":-)" Liber wrote:On 8 January 2013 03:35, Nicol Bolas <jmck...@gmail.com> wrote:
If the above compiles, then clearly the design of the feature is to be usable for more than just aggregate and list initialization. It's there to solve the Most Vexing Parse issue
How does {} not solve the most vexing parse issue?
Because you can't use it everywhere.
As pointed out in the proposal, the shadowing effect of {}'s constructor picking rules means that there's no way to call certain constructors with {}.
On 8 January 2013 13:14, Nicol Bolas <jmck...@gmail.com> wrote:
On Tuesday, January 8, 2013 7:57:16 AM UTC-8, Nevin ":-)" Liber wrote:On 8 January 2013 03:35, Nicol Bolas <jmck...@gmail.com> wrote:
If the above compiles, then clearly the design of the feature is to be usable for more than just aggregate and list initialization. It's there to solve the Most Vexing Parse issue
How does {} not solve the most vexing parse issue?
Because you can't use it everywhere.
Why can't I use it everywhere where I pass *zero* parameters?
As pointed out in the proposal, the shadowing effect of {}'s constructor picking rules means that there's no way to call certain constructors with {}.
And your proposal still doesn't fix that in 100% of the cases. Take the following class:struct S {S();S(initializer_list<int>);S(initializer_list<double>);};How do I call S(initializer_list<int>) with your proposed solutions on a 0 element initializer list?
And I still don't understand why I'd want to design a class where S() vs. S{} vs. S{{}} results in different objects; seems very fragile to me.
Vidar Hasfjord
Hi Nicol,I am sympathetic to your desire to address this issue, especially the constructor selection surprise when brace-initialization is involved, and the indeterminism of the rules in a template context, where, as you point out, the selection between a constructor taking an initializer_list and ordinary constructors depends on the argument type. But, like other respondents in this discussion, I do not like the added complexity and syntax that your solution proposes.
My main interest, as discussed in this post, is in fixing the surprise and indeterminism mentioned; not make initialization more uniform (although I touch on the latter at the end). I apologise if you consider this a further diversion of the primary aim of your proposal.
I haven't studied the issue closely, but my immediate alternative proposal is to no longer prefer the constructor taking initializer_list where there are viable ordinary constructors. The compiler should simply treat it as ambiguous and refuse it.
This solution would change the mentioned surprise and indeterminism into straight-forward ambiguity. As always, ambiguity in constructor overload resolution is a class design issue. In other words, this solution acknowledges that std::vector has a slight flaw in its design, and revokes the special rule that tries to paint over it.
A change of the initializer_list preference rule will of course break (some new C++11) code, although this may be acceptable if there is a simple way to disambiguate.
Example:
vector <int> a {42}; // Error: Constructor selection is ambiguous under the proposed rule change.
Existing ways to disambiguate:
vector <int> a (42); // OK: Pass size.vector <int> a ({42}); // OK: Pass single element.
An alternative way to disambiguate in favour of initializer_list may be to use a trailing comma:
vector <int> a {42,}; // OK: Pass single element.
This solution requires no new syntax, but lacks a way to disambiguate in favour of ordinary constructors when using brace-initialization.
My gut feeling is that any additional syntax to improve on this issue should be considered in a broader context, e.g. in conjunction with named arguments:
vector <int> a {size := 42}; // OK: Pass size.
Existing ways to disambiguate:
vector <int> a (42); // OK: Pass size.vector <int> a ({42}); // OK: Pass single element.
An alternative way to disambiguate in favour of initializer_list may be to use a trailing comma:
vector <int> a {42,}; // OK: Pass single element.
My gut feeling is that any additional syntax to improve on this issue should be considered in a broader context
The following code
std::vector<int *> v({42});
std::cout << v.size() << std::endl;
will print 42.
[addintional syntax should be considered in a broader context] That's my feeling too. Initializer lists with expression semantics could solve ambiguity issues and forwarding issues simultaneously.
Hi Nicol,
I think the strength of your proposal is the motivation part where you point out, in my view, a very serious problem. The weakness of your proposal is your suggested solution. The proposed rules and syntax seem to put off many readers, as seen by much of the feedback so far, and I fear that the real and serious problem may be overlooked in the process.First, let me speak up for ambiguity, since you seem to so readily dismiss it. Unresolved ambiguity is a very important feature of C++. It is the safety valve that makes many of the powerful features of C++ viable, such as overloading and template specialization. The refusal to compile code that has an apparent ambiguity makes it manageable to reason about C++ code, even in the face of complex lookup, overload resolution and template instantiation. This feature is a corner stone of C++.The evil twin of unresolved ambiguity is arbitrarily resolved ambiguity. When the language invents a rule that resolves an apparent ambiguity, the programmer needs to know the rule and how it applies in each particular case of code. This is problematic. The rule may not be intuitive in all cases. Code evolves. And sometimes there may not even be enough information in the code to determinate the meaning, until the code itself is used in a particular case. As you point out in the motivation for your proposal, the indeterminism of constructor selection for vector <T> {20} in a template context is a poignant example. This is bad.
The initializer_list-preference rule for constructor selection in brace-initialization is an arbitrary resolution of an apparent ambiguity, which has surprising and indeterminate effects, and therefore qualifies as a language defect in my view.As far as I know, the rule was solely introduced to paint over an ambiguity problem in the standard library. But, as many have pointed out, the library would not have this ambiguity if it had been designed with initializer_list in mind. There are many ways to avoid this ambiguity in the library, in the same way ambiguities in overload resolution always have been dealt with.So, I wish you well in defending your proposal, but I hope you will focus as much on the problem as you do on your particular solution. Let me suggest a two-step approach:1. Get the problem recognized as a serious language defect, and propose revoking the rule that causes it.
2. Propose features to make brace-initialization more powerful, as to fulfil its promise of uniform initialization.
I consider (1) most important, but let me end with some adventurous brain-storming about (2). In my last post, I did briefly propose piggy-backing on a hypothetical "named arguments" feature:
vector <int> a {size := 42}; // OK: Pass size.
I then allowed the actual parameter name to be dropped:
vector <int> a {:= 42}; // OK: Pass size.
This then becomes bare-bones syntax to indicate that an argument should be explicitly matched against a parameter list. Since 42 is not an initializer_list, this syntax enables the disambiguation in favour of ordinary constructors, while keeping the possibility open for the syntax to be extended in the future to support named arguments. Note that this syntax is similar to yours, and will reap the full benefit of brace-initialization (non-narrowing, etc.).
Now, let's go crazy and allow the elision of the braces for the single-argument case:
vector <int> a := 42; // OK: Pass size.
Voilà! We have derived a new simple, intuitive and well-known syntax for new-style non-narrowing initialization! This syntax is as intuitive and simple as the old "=" syntax, and does not make dual use of the assignment operator token "=", nor does it need the special copy-initialization rules for the "=" syntax.
std::vector<int> v{c: ...};
std::vector<int> v2 = {c: ...}; //Well, if it weren't explicit.
std::vector<int> Func() {return {c: ...}; } //Again, if it weren't explicit.std::vector<int> v := ...; //But only if ... is one argument.
std::vector<int> v {:= ...}; //For multi-argument ...
std::vector<int> v2 = {:= ...}; //Why are there two = here?
std::vector<int> Func() {return {:= ...}; }The syntax even works well with initializer-lists:
vector <int> a := {42}; // OK: Pass single element.
This works fine because it expands to an explicit argument match for an initializer_list:
vector <int> a {:= {42}}; // OK: Pass single element.
> You can't initialize aggregates with ()
Perhaps this warrants special consideration? Is there any reason why this should not be allowed? E.g:
struct Abc {int a, b, c;};Abc abc (1, 2, 3);
Conceptually, the language only needs to add an implicit constructor. This would solve the emplace_back problem you demonstrated, I presume.
> Honestly, I'm of the opinion that, if we can't fix uniform initialization syntax to actually be uniform and functionally replace all other initialization everywhere, then we should just deprecate and remove it ASAP.
Well, beware of throwing out the baby with the bathwater. Brace-initialization syntax solves issues other than uniformity, such as narrowing and the most-vexing-parse.
Note that I'm not here to shoot your proposal down. I think your proposal has merit, and I wish you well in its progress, if not in the current form.> Giving the user the ability to resolve the ambiguity themselves should not be contingent on making the ambiguity illegal. The former is the most important thing, because that is what allows the user to actually solve the problem.The language already has ample features to deal with ambiguity, as long as you are willing to use them, i.e. not restrict yourself to brace-initialization. So users can survive until an ideal solution for brace-initialization is found.
Also, as many have argued, improvements in the library can be made to resolve mentioned ambiguities in std::vector, even with the consistent use of brace-initialization. I don't argue that any of these options are preferable to your solution, but they are undeniable available.
> What good is having a solution that you can't reliably use?Well, perfection is the enemy of progress. I think it is often a good approach to do things piecemeal. Often the first step is something everyone can agree on, while further steps are more difficult, or too early in their exploration, to find consensus.
Hi Nicol,I get all your arguments for improving uniform initialization so that overload resolution can select ordinary constructors before il-constructors. Consider me convinced.What I didn't get is why you are downplaying the importance of fixing the language so that it ensures that existing code, that already suffers the very problems you so poignantly highlight in the motivating part of your proposal, no longer compiles with silent bugs.> The last thing we want is for the committee to just ban the ambiguity, thus making the problem not sufficiently urgent. Without pressure to get a real fix, people can spend 10 years arguing over different approaches to no avail.OK. Now I understand where you are coming from on this. Yes, by all accounts, it can take an awful lot of time and effort to get things through the standardisation process. That said, I think this is unavoidable for any extensive proposal like yours.
And some design space exploration and scrutiny may actually be good.
Regarding revoking the il-preference rule, our main difference of opinion, consider this scenario:After successful promotion your proposal is accepted. If it doesn't catch the bus for C++14, which in all probability is unlikely for such an extensive feature, then you are looking at C++17. That's 4 to 5 years away. In the meantime code is written for C++11 and C++14. Since nothing is done to prevent silent bugs caused by this issue, broken code will prevail, and novices will be stumped. Further more, only code that is rewritten using C++17 with this issue in mind, may fix the bugs. Legacy code may persist for ever with such silent bugs, even if compiled with the latest C++17 compilers.
I argue that we should seek to eliminate those silent bugs. If at all possible, try to achieve that in C++14. Then move on to improve uniform initialization further.
On Saturday, January 26, 2013 3:11:15 AM UTC+4, vattila...@yahoo.co.uk wrote:
Existing ways to disambiguate:
vector <int> a (42); // OK: Pass size.vector <int> a ({42}); // OK: Pass single element.
For the latter initialization there are two viable constructors:
explicit vector(size_type n);
vector(initializer_list<T>, const Allocator& = Allocator());
> You can't initialize aggregates with ()
Perhaps this warrants special consideration? Is there any reason why this should not be allowed? E.g:
struct Abc {int a, b, c;};Abc abc (1, 2, 3);
Conceptually, the language only needs to add an implicit constructor. This would solve the emplace_back problem you demonstrated, I presume.
So, in the case the il-preference rule was revoked, then vector <int> ({42}) would not be sufficient to disambiguate in favour of the il-constructor. But you could dovector <int> a = {42}; // OK: Pass single element (copy-initialization excludes explict constructors).
orvector <int> a (initializer_list <int> {42}); // OK: Pass single element.Which is a mouthful
void Func(const std::vector &v) {...}
Func( (42) ); //Does not work.
Func( {42} ); //"works" but potentially does the wrong thing.
Func( {l: 42} ); //Does the right thing.
Func( {c:42} ); //Does work. Well, if it weren't an explicit constructor...On Saturday, January 26, 2013 11:53:48 PM UTC+4, vattila...@yahoo.co.uk wrote:> You can't initialize aggregates with ()
Perhaps this warrants special consideration? Is there any reason why this should not be allowed? E.g:
struct Abc {int a, b, c;};Abc abc (1, 2, 3);
Conceptually, the language only needs to add an implicit constructor. This would solve the emplace_back problem you demonstrated, I presume.
I think that aggregate initialization and initialization by an initializer-list constructor should look similar, because they are supposed to perform an itemwise initialization, while initialization with () is commonly used for an arbitrary initialization.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.To post to this group, send email to std-pr...@isocpp.org.
I think I have a solution that doesn't require a new syntax.
What I would propose is that the list-initialization preference is dropped for template types. For non-template types and for fully specialized templates we keep the current rules.
That is all. I think that solves this problem cleanly.
What this means is that you can write this:
template<typename T, typename... Arguments>
T construct_uniform(Arguments &&... arguments)
{
// we don't know the type of T, so this will not prefer initializer_list construction
return T{std::forward<Arguments>(arguments)...};
}
And it does what you expect.
For example
construct_uniform<std::vector<float>>(10); // create vector with ten elements
construct_uniform<std::vector<int>>(10); // create vector with ten elements
construct_uniform<std::vector<int>>({10}); // create vector with one element
std::vector<float> ten_floats{10}; // create vector with ten elements
std::vector<int> one_int{10}; // create vector with one element
With the idea being that you know what you are doing when you are writing code with fully specialized types.
template<typename T, typename... Arguments>
typename std::enable_if<std::is_aggregate<T>::value, T>::type construct_uniform(Arguments &&... arguments)
{
//T is an aggregate, so use uniform initialization.
return T{std::forward<Arguments>(arguments)...};
}
template<typename T, typename... Arguments>
typename std::enable_if<!std::is_aggregate<T>::value, T>::type construct_uniform(Arguments &&... arguments)
{
//T is not an aggregate, so call a constructor.
return T(std::forward<Arguments>(arguments)...);
}
SomeFunc(arg1, {c:...}, arg3);
On Monday, January 28, 2013 4:19:30 AM UTC-8, Nikolay Ivchenkov wrote:
I think that aggregate initialization and initialization by an initializer-list constructor should look similar, because they are supposed to perform an itemwise initialization, while initialization with () is commonly used for an arbitrary initialization.
Except that it isn't; it's only used for calling constructors.
It can't initialize aggregates.
The construct_uniform was just an example to illustrate the effects of my proposed change. It is not part of the proposal.
The proposal is to drop the list initialization preference for template types and partially specialized templates. Non-template types and fully specialized templates keep the current behavior.
Now read that email again for examples.
You will find that that change solves all the problems mentioned in this thread without requiring new syntax.
Yes, it would mean that you have to fall back to () initialization sometimes, (as you do now) but it is superior to the current uniform initialization rules in that it doesn't give you unexpected behavior.
--
The construct_uniform was just an example to illustrate the effects of my proposed change. It is not part of the proposal.
The proposal is to drop the list initialization preference for template types and partially specialized templates. Non-template types and fully specialized templates keep the current behavior.
Now read that email again for examples.
You will find that that change solves all the problems mentioned in this thread without requiring new syntax.
void SomeFunc(std::vector<int> v);
SomeFunc({10});Yes, it would mean that you have to fall back to () initialization sometimes,
(as you do now) but it is superior to the current uniform initialization rules in that it doesn't give you unexpected behavior.
std::vector<int> v{20};On Tuesday, January 29, 2013 5:04:06 AM UTC+4, Nicol Bolas wrote:On Monday, January 28, 2013 4:19:30 AM UTC-8, Nikolay Ivchenkov wrote:
I think that aggregate initialization and initialization by an initializer-list constructor should look similar, because they are supposed to perform an itemwise initialization, while initialization with () is commonly used for an arbitrary initialization.
Except that it isn't; it's only used for calling constructors.
And such constructors may perform a non-itemwise initialization.
It can't initialize aggregates.
Actually it can initialize aggregates, but only with another instance:
#include <iostream>
#include <string>
struct A
{
std::string s;
int n;
};
int main()
{
A a({"text", 1});
std::cout << a.s << "; " << a.n << std::endl;
}
Here 'a' initialized with a temporary object, which is initialized with the braced-init-list. It's possible to modify the rules so that AggregateType({items...}) would be equivalent to AggregateType{items...}.
On Tue, Jan 29, 2013 at 1:58 AM, Nicol Bolas <jmck...@gmail.com> wrote:
> On Monday, January 28, 2013 6:30:54 AM UTC-8, Sebastian Gesemann wrote:
>>
>> Nicol Bolas wrote:
>> > [...]
>>
>> I'm sorry, I still don't get it. How can the use of
>> T {l: ...}
>> T {c: ...}
>> instead of
>> T {...}
>> T (...)
>> be considered more uniform? It does not appear to solve any problem.
>> What did I miss?
>
> You missed the fact that `T(...)` requires calling a constructor. `T{c:...}`
> can use aggregate initialization if `T` is an aggregate.
> The point is the ability to initialize aggregates or non-aggregates in the
> exact same way.
If you don't care about whether T is an aggregate or not, what is
stopping you from using the plain C++11 curly braces for
initialization?
::new (static_cast<void*>(p)) T(std::forward<Args>(args)...)::new (static_cast<void*>(p)) T{std::forward<Args>(args)...}
::new (static_cast<void*>(p)) T{c: std::forward<Args>(args)...}
> Also, there's the fact that you can't use `()` syntax without a
> typename:
>
> void Func(const std::vector &v) {...}
>
> Func( (42) ); //Does not work.
This is a copy initialization context for 'v' and the constructor is
explicit. I hope nobody actually wants this to invoke the constructor
that takes a size parameter because that's the point of having
explicit constructors.
> Func( {42} ); //"works" but potentially does the wrong thing.
By "potentially does the wrong thing" I guess you mean that the
constructor taking a value for the size might be invoked by accident
here. If so, You are wrong on this one because this constructor is
explicit which is why it's not viable in this context.
> Func( {l: 42} ); //Does the right thing.
We don't need the explicit 'l:' in this case to make the compiler
prefer std::initializer_list constructors.
Uniform initialization syntax is not meant to be used only for "itemwise initialization".
It was, as stated in N2532, to be used in all cases of initialization. The goal here is to make it usable in all cases of initialization.
If you want to argue against this goal, take it up with Stroustrup.
It can't initialize aggregates.
Actually it can initialize aggregates, but only with another instance:
#include <iostream>
#include <string>
struct A
{
std::string s;
int n;
};
int main()
{
A a({"text", 1});
std::cout << a.s << "; " << a.n << std::endl;
}
Here 'a' initialized with a temporary object, which is initialized with the braced-init-list. It's possible to modify the rules so that AggregateType({items...}) would be equivalent to AggregateType{items...}.
So you want to change the rules of how copy constructors for aggregates work
Here's a completly different idea for a solution: Disable
std::list_initialiter list constructor consideration by using a
special first element of a {}-list that will then be discarded during
overload resolution for picking the right constructor.
vector<int> v {nolist,42};
It could be made to work even for the forwarding case so that we don't
have to specify so many additional functions:
template<class T, class...Args>
unique_ptr<T> make_unique(Args&&...args)
{
return unique_ptr<T>(new T{forward<Args>(args)...});
}
vector <int> a = {42}; // OK: Pass single element (copy-initialization excludes explict constructors).
1) Explicit constructors are still considered here.
2) In the following case
vector <int> a = {42, 1};
there are two viable converting constructors.
> The general rule should be: Don't overload too eagerly.Yes, when it comes to the demonstrated ambiguities in std::vector <int>, the fact remains that no well-designed class interface should introduce these ambiguities in the first place.
So this design problem may end up being dealt with by coding standards, i.e. similar to the current "always include a virtual destructor in your polymorphic base class", we will have "always do this-that-and-the-other if you include an il-constructor in your class".
On Monday, 28 January 2013 12:19:30 UTC, Nikolay Ivchenkov wrote:vector <int> a = {42}; // OK: Pass single element (copy-initialization excludes explict constructors).
1) Explicit constructors are still considered here.Are you sure? Isn't this a copy-initialization context? At least, GCC 4.5.1 does not consider the explicit constructor here:
I'm sure, and the diagnostic message clearly says that the explicit constructor is selected.
On Tuesday, January 29, 2013 5:02:20 PM UTC+4, Nicol Bolas wrote:
Uniform initialization syntax is not meant to be used only for "itemwise initialization".
So what? You are suggesting two different kinds of initialization, rather than one uniform initialization syntax (as several people already noticed).
On Tuesday, 29 January 2013 14:41:29 UTC, Sebastian Gesemann wrote:> Anyways, tbh, it doesn't change how I feel about your proposal. It> does not seem to fit well into the rest of c++ and the need to special> case functions like make_shared or construct is bad.On a related note, I just browsed the C++ Standard Library Active Issues List (N3516) and noticed issue 2089, "std::allocator::construct should use uniform initialization".It proposes to solve the problem in emplace_back (currently not allowing aggregate-initialization and list-initialization) by meta-programming using type traits. These programming techniques are now part of a well-established toolbox for controlling overload resolution, and the mood may very well be that this is sufficient to deal with the initialization quirks in the language as well.
> The general rule should be: Don't overload too eagerly.Yes, when it comes to the demonstrated ambiguities in std::vector <int>, the fact remains that no well-designed class interface should introduce these ambiguities in the first place. So this design problem may end up being dealt with by coding standards, i.e. similar to the current "always include a virtual destructor in your polymorphic base class", we will have "always do this-that-and-the-other if you include an il-constructor in your class".
> The general rule should be: Don't overload too eagerly.Yes, when it comes to the demonstrated ambiguities in std::vector <int>, the fact remains that no well-designed class interface should introduce these ambiguities in the first place. So this design problem may end up being dealt with by coding standards, i.e. similar to the current "always include a virtual destructor in your polymorphic base class", we will have "always do this-that-and-the-other if you include an il-constructor in your class".
Or, you know, we could just fix the problem. The problem is only ambiguous because the user cannot accurately express what they want. Instead of forcing every writer of a class to prevent the ambiguity, let's just allow programmers to say what it is that they actually want.
I don't understand this resistance to such an obvious language fix. The language doesn't let us say what we really want to do; the obvious solution is to allow us to say what we really want to do. This isn't rocket science here.
Thinking like you're doing is what gives us nightmare library features like `std::enable_if` and other similar metaprogramming garbage. It's always easier to just make everyone else do more work than to fix the problem right. Yes, they work, but they're horrible to read and painful to use.
We as C++ programmers need to stop relying on these quick-fix solutions. Isn't that why the C++ committee is moving to a faster release cycle? So that we can get real fixes for real problems the right way, in a timely manor?
void Func(int i);
void Func(float f);
Func(1.0); //Ambiguous call
Func((float)1.0); //Ambiguity resolved
Or, you know, we could just fix the problem. The problem is only ambiguous because the user cannot accurately express what they want. Instead of forcing every writer of a class to prevent the ambiguity, let's just allow programmers to say what it is that they actually want.
I don't understand this resistance to such an obvious language fix. The language doesn't let us say what we really want to do; the obvious solution is to allow us to say what we really want to do. This isn't rocket science here.
Thinking like you're doing is what gives us nightmare library features like `std::enable_if` and other similar metaprogramming garbage. It's always easier to just make everyone else do more work than to fix the problem right. Yes, they work, but they're horrible to read and painful to use.
We as C++ programmers need to stop relying on these quick-fix solutions. Isn't that why the C++ committee is moving to a faster release cycle? So that we can get real fixes for real problems the right way, in a timely manor?
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/?hl=en.
On 29 January 2013 21:17, Nicol Bolas <jmck...@gmail.com> wrote:
Or, you know, we could just fix the problem. The problem is only ambiguous because the user cannot accurately express what they want. Instead of forcing every writer of a class to prevent the ambiguity, let's just allow programmers to say what it is that they actually want.
I don't understand this resistance to such an obvious language fix. The language doesn't let us say what we really want to do; the obvious solution is to allow us to say what we really want to do. This isn't rocket science here.
*Everyone* who posts here thinks they have the "obvious" solution.
Thinking like you're doing is what gives us nightmare library features like `std::enable_if` and other similar metaprogramming garbage. It's always easier to just make everyone else do more work than to fix the problem right. Yes, they work, but they're horrible to read and painful to use.
Okay, I'll bite. What's your "obvious" solution to enable_if? The last solution, Concepts, is still years in the making...
On Tuesday, January 29, 2013 6:28:34 AM UTC-8, Nikolay Ivchenkov wrote:You are suggesting two different kinds of initialization, rather than one uniform initialization syntax (as several people already noticed).
I'm suggesting two slight variations of initialization syntax
so that the user can express what they're getting.
The only difference is which constructors are considered; all of the other aspects of initialization (initializing aggregates, figuring out what type to use, etc) remain the same. In short, 90% of the text around such braced-init-lists in the standard will be identical; all that changes is the part of 13.3.1.7 that says which constructors are considered.
No, I would like to have a modifier ~, or +, or something else for a braced-init-list such that
AggregateType a = ~ braced-init-list;
and
AggregateType a ( ~ braced-init-list );
That's what {l:} does.
Furthermore, {l:} is not enough, because there's no way to have similar behavior but only consider regular constructors.
Of course, you don't want that because you don't want uniform initialization syntax at all. You don't want users to use {} syntax everywhere to initialize things.
Personally, I've never understood this notion that aggregate initialization is in any way like initializer_list initialization. It's far more like constructor syntax than initializer_lists, since aggregates don't have to contain the same types.
That's why {} should only have considered regular constructors to begin with, forcing the use of {{}} for initializer_list initialization.
On Tuesday, January 29, 2013 10:04:51 AM UTC-8, vattila...@yahoo.co.uk wrote:On Tuesday, 29 January 2013 14:41:29 UTC, Sebastian Gesemann wrote:> Anyways, tbh, it doesn't change how I feel about your proposal. It> does not seem to fit well into the rest of c++ and the need to special> case functions like make_shared or construct is bad.On a related note, I just browsed the C++ Standard Library Active Issues List (N3516) and noticed issue 2089, "std::allocator::construct should use uniform initialization".It proposes to solve the problem in emplace_back (currently not allowing aggregate-initialization and list-initialization) by meta-programming using type traits. These programming techniques are now part of a well-established toolbox for controlling overload resolution, and the mood may very well be that this is sufficient to deal with the initialization quirks in the language as well.
....
Granted, there would be no need for this deduction stuff if they had {c:}, which would be effectively equivalent (save never using initializer_list constructors without explicitly specifying it, but I would consider that an improvement)
On Tuesday, January 29, 2013 7:34:12 PM UTC-8, Nevin ":-)" Liber wrote:
Okay, I'll bite. What's your "obvious" solution to enable_if? The last solution, Concepts, is still years in the making...
static if.
It's already in development, with successful implementations in another language in a similar capacity.
On Wednesday, January 30, 2013 7:05:50 AM UTC+4, Nicol Bolas wrote:On Tuesday, January 29, 2013 6:28:34 AM UTC-8, Nikolay Ivchenkov wrote:You are suggesting two different kinds of initialization, rather than one uniform initialization syntax (as several people already noticed).
I'm suggesting two slight variations of initialization syntax
That sounds like "slightly pregnant". {c:} and {l:} are two different syntactic constructs. Similarly, static_cast<T>(x) and const_cast<T>(x) are two different syntactic constructs.
so that the user can express what they're getting.
This goal can be achieved by replacing one pseudo-uniform initialization syntax with a combination of _different_ syntactic constructs. {c:} + {l:} is one of possible combinations, but, IMO, it's not the best option.
The only difference is which constructors are considered; all of the other aspects of initialization (initializing aggregates, figuring out what type to use, etc) remain the same. In short, 90% of the text around such braced-init-lists in the standard will be identical; all that changes is the part of 13.3.1.7 that says which constructors are considered.
If that's so simple, why don't you provide exact wording changes?
No, I would like to have a modifier ~, or +, or something else for a braced-init-list such that
AggregateType a = ~ braced-init-list;
and
AggregateType a ( ~ braced-init-list );
That's what {l:} does.
Your description of {l:} doesn't imply that.
“l”: Call an initializer_list constructor.
These variation only affect which constructors are considered during overload resolution; in every other respect, they work like a regular braced-init-list.
Personally, I've never understood this notion that aggregate initialization is in any way like initializer_list initialization. It's far more like constructor syntax than initializer_lists, since aggregates don't have to contain the same types.
...while objects with arbitrary constructors don't have to contain any sequences at all. What is common between aggregates and objects with arbitrary constructors? Why should we use different syntax for built-in arrays / std::array and std::vector?
int arr[] = {c: 10, 20};
std::array<int, 2> a{c: 10, 20};
std::vector<int> v{l: 10, 20};
std::vector<int> v1{l: 10, 20};
std::vector<int> v2{c: 10, 20};template<typename T>
void Func()
{
T arr{l: 10, 20};
//Do stuff with arr
}
void Func<int[2]>();
void Func<std::array<int, 2>>();
void Func<std::vector<int>>();
That's why {} should only have considered regular constructors to begin with, forcing the use of {{}} for initializer_list initialization.
You'll get similar issue with nested braced-init-list: {x} can match parameters of type std::size_t and of type std::initializer_list<T>.
On Wednesday, January 30, 2013 7:17:55 AM UTC+4, Nicol Bolas wrote:On Tuesday, January 29, 2013 10:04:51 AM UTC-8, vattila...@yahoo.co.uk wrote:On Tuesday, 29 January 2013 14:41:29 UTC, Sebastian Gesemann wrote:> Anyways, tbh, it doesn't change how I feel about your proposal. It> does not seem to fit well into the rest of c++ and the need to special> case functions like make_shared or construct is bad.On a related note, I just browsed the C++ Standard Library Active Issues List (N3516) and noticed issue 2089, "std::allocator::construct should use uniform initialization".It proposes to solve the problem in emplace_back (currently not allowing aggregate-initialization and list-initialization) by meta-programming using type traits. These programming techniques are now part of a well-established toolbox for controlling overload resolution, and the mood may very well be that this is sufficient to deal with the initialization quirks in the language as well.
....
Granted, there would be no need for this deduction stuff if they had {c:}, which would be effectively equivalent (save never using initializer_list constructors without explicitly specifying it, but I would consider that an improvement)
We already found out that simple replacement of (items...) with {c: items...} is able to break a lot of existing code. You may be sure that if such a change will be accepted, many people will curse the author of the proposal and the committee.
On Wed, Jan 30, 2013 at 4:25 AM, Nicol Bolas wrote:
> Indeed, arguing against having a fix is like saying that we shouldn't allow
> this to work:
>
> void Func(int i);
> void Func(float f);
>
> Func(1.0); //Ambiguous call
> Func((float)1.0); //Ambiguity resolved
>
> My proposal is conceptually no different than this.
Actually, it is different. In this example you did not change the
initialization syntax, but just an argument's expression. The
initialization syntax and the argument expressions are two separate
things.
You propose to add two additional initialization syntaxes
instead of making one syntax work for more cases. It's a hack, a quick
fix for odd overload resolution rules and vector<int> unfortunately
relying on these odd overload resolution rules to disambiguate between
two constructors merely based on the kind of initialization syntax and
not the list of arguments like it probably should.
Your fix stops
working when you have to think about how you should implment
allocator<>::construct. Ideally the user of this construct function
template is able to control how the object should be initialized by
using appropriate arguments so that overload resolution picks the
right one. You just added yet another mechanism of control that
doesn't work in the contexts like allocator<>::construct unless you
add special casing for {c:} and {l:} via providing construct_l and
construct_c or something like this. This special casing would be
viral, too. You'd need this special casing also in make_shared and
emplace_back for example. But this special casing would already be
possible in C++11 today without {c:} and {l:}.
I think the idea of adding a first dummy argument for disambiguation
is better because it does not invent new initialization syntaxes but
just make one syntax work for all cases.
But it's still not enough
bang for the buck if you ask me. This also seems like yet another hack
that just tries to cover up funny overload resolution rules.
> We as C++ programmers need to stop relying on these quick-fix
> solutions.
Including yours?
At some level, we are going to have to accept that the forwarding problem can never be fully resolved.
IYAM, the simplest way to deal with aggregates is to simply cut aggregate initialization, and then say that aggregates have implicitly generated explicit constructors.
On Wednesday, January 30, 2013 5:50:25 AM UTC-8, Nikolay Ivchenkov wrote:On Wednesday, January 30, 2013 7:05:50 AM UTC+4, Nicol Bolas wrote:On Tuesday, January 29, 2013 6:28:34 AM UTC-8, Nikolay Ivchenkov wrote:You are suggesting two different kinds of initialization, rather than one uniform initialization syntax (as several people already noticed).
I'm suggesting two slight variations of initialization syntax
That sounds like "slightly pregnant". {c:} and {l:} are two different syntactic constructs. Similarly, static_cast<T>(x) and const_cast<T>(x) are two different syntactic constructs.
{c:} and {l:} are conceptually like the difference between static_cast<float> and static_cast<int> than static_cast and const_cast.
They are two mechanisms that operate similarly: they modify how their parameter is interpreted by subsequent code. Which modification they perform is based on the parameter.
so that the user can express what they're getting.
This goal can be achieved by replacing one pseudo-uniform initialization syntax with a combination of _different_ syntactic constructs. {c:} + {l:} is one of possible combinations, but, IMO, it's not the best option.
My point is that your opinion is based on your belief that initialization should not be uniform,
The only difference is which constructors are considered; all of the other aspects of initialization (initializing aggregates, figuring out what type to use, etc) remain the same. In short, 90% of the text around such braced-init-lists in the standard will be identical; all that changes is the part of 13.3.1.7 that says which constructors are considered.
If that's so simple, why don't you provide exact wording changes?
First, I never said that the wording was "simple"; I said that it was localized.
Personally, I've never understood this notion that aggregate initialization is in any way like initializer_list initialization. It's far more like constructor syntax than initializer_lists, since aggregates don't have to contain the same types.
...while objects with arbitrary constructors don't have to contain any sequences at all. What is common between aggregates and objects with arbitrary constructors? Why should we use different syntax for built-in arrays / std::array and std::vector?
int arr[] = {c: 10, 20};
std::array<int, 2> a{c: 10, 20};
std::vector<int> v{l: 10, 20};
Because the user needs that specificity for objects with constructors:std::vector<int> v1{l: 10, 20};
std::vector<int> v2{c: 10, 20};
These do different things.
If I'm in a template context and I have some type T, which may or may not be an aggregate, I need to tell the compiler which one I mean.
Thus:template<typename T>
void Func()
{
T arr{l: 10, 20};
//Do stuff with arr
}
void Func<int[2]>();
void Func<std::array<int, 2>>();
void Func<std::vector<int>>();
Where `T` is any form of container which can be initialized with {l:} syntax with two elements.
Your way would require that I do some std::enable_if detection and so forth to figure out if `T` is an aggregate and use a different codepath or whatever.
We already found out that simple replacement of (items...) with {c: items...} is able to break a lot of existing code. You may be sure that if such a change will be accepted, many people will curse the author of the proposal and the committee.
Did we? In what case would {c:} usage for all of the currently constructor-based initialization break code?
IYAM, the simplest way to deal with aggregates is to simply cut aggregate initialization, and then say that aggregates have implicitly generated explicit constructors.
Cheers!
SG
Cheers!
SG
On Wed, Jan 30, 2013 at 10:23 PM, Nicol Bolas wrote:
> On Wednesday, January 30, 2013 5:36:21 AM UTC-8, Sebastian Gesemann wrote
> Furthermore, different library authors will have different ideas about
> different syntax.
> Some will prefer the first argument to disambiguate. Some
> will use the last. They will use different types, thus forcing you to use a
> different type name for disambiguation for different libraries. And so
> forth.
Oh, sorry, I guess I wasn't clear enough and you misunderstood. I was
not talking about a library solution. I was talking about giving the
type of "nolist" special treatment from the compiler in the following
way:
struct foo {
foo(int); // #1
foo(initializer_list<int>) // #2
};
int main() {
foo x {nolist,29}; // picks #1
foo y {17}; // picks #2
}
without a constructor every receiving a "nolist" parameter. The beauty
of it is that we actually don't have to special case this in
forwarding code since nolist is just one of the parameters. This would
be an example of a truly uniform initialization where all the
information that is used in deciding which overload it should be
resolved to is encoded in the types and value categories of the
arguments to that list which are forwardable without problems.
On Thursday, January 31, 2013 12:26:58 PM UTC, Sebastian Gesemann wrote:
> Oh, sorry, I guess I wasn't clear enough and you misunderstood. I was
> not talking about a library solution. I was talking about giving the
> type of "nolist" special treatment from the compiler in the following
> way:
>
> struct foo {
> foo(int); // #1
> foo(initializer_list<int>) // #2
> };
>
> int main() {
> foo x {nolist,29}; // picks #1
> foo y {17}; // picks #2
> }
As a further exploration of the design space, rather than making `nolist` an instance of a special type with special treatment from the compiler, could we make `__nolist` a new cv-qualifier with the function std::nolist to do the conversion (thus getting similarity with std::move and std::forward). For example,
struct foo {
foo(int); // #1
foo(initializer_list<int>) // #2
};
int main() {
foo x {nolist(29)}; // picks #1
foo y {17}; // picks #2
}
Here `nolist(29)` casts `29` from `int` to `__nolist int`, which is convertible to `int`, but not `initializer_list <int>`, for overload resolution.
Regards,
Vidar Hasfjord
On Thursday, January 31, 2013 3:44:34 PM UTC+4, DeadMG wrote:It has one parameter for each data member which is the type of that data member that is then forwarded to it. So in the case of X, it would beX(std::string __s1 = std::string(), std::string __s2 = std::string(), std::unique_ptr<std::string> __s3 = std::unique_ptr<std::string>()): s1(std::forward<std::string>(__s1)), s2(std::forward<std::string>(__s2)), p(std::forward<std::unique_ptr<std::string>>(__s3)) {}Considering, however, immovable types, perhaps it would be more accurate (although less convenient) to specify asX(std::string __s1): s1(std::forward<std::string>(s1)), s2(), p() {}X(std::string __s1, std::string __s2): s1(std::forward<std::string(__s1)), s2(std::forward<std::string>(__s2)), p() {}This should retain the correct semantics in terms of immovable types, and types which behave differently when value-initialized, such as int.
Is that more simple than just defining initialization
AggregateType x(items...);
to be equivalent to
AggregateType x{items...};
? (Note that I don't suggest to change the rules so)
And how about efficiency? Your two-step initialization of members is potentially less efficient than one-step initialization that takes place in an aggregate initialization.