RFC: Towards More Uniform Initialization proposal

720 views
Skip to first unread message

Nicol Bolas

unread,
Dec 30, 2012, 4:49:21 PM12/30/12
to std-pr...@isocpp.org
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.

Introduction

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.

Motivation and Scope

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.

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.

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.

Impact on the Standard

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.

Design Decisions

Bikeshedding

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.

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.

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.

Alternate Designs

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.

Change the Definition

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.

Towards More Uniform Initialization.html

Klaim - Joël Lamotte

unread,
Dec 30, 2012, 7:06:44 PM12/30/12
to std-pr...@isocpp.org
Hi,

I'm not a C++ committee member but this document seems very convincing to me.
(and scary where the problems are exposed).

A small suggestion to add to the bikeshedding list when this will be discussed: using ':' 
std::vector<int> v = {: 1, 2, 3 };

':' is not an operator so it makes things less ambiguous than operators like ^ or | that look like some mathematic or bit-wise operation is happening, even if the syntax is still invalid C++11.

Actually I don't really "like" having another strange syntax, but so far I don't see how this could be fixed without the silently breaking change.
Maybe there's a way to do this change and add another change that would make the first change non-silent? 

Joel Lamotte

Nicol Bolas

unread,
Dec 30, 2012, 8:18:38 PM12/30/12
to std-pr...@isocpp.org

The only way to make it a noisy change would be to force {} to only use initializer_list constructors (or aggregate initialization). That is, it could never select a non-initializer_list constructor. I'll add it to the list of alternatives, but I highly doubt they'd go for it.

Joel Lamotte

Nevin Liber

unread,
Dec 31, 2012, 4:15:05 PM12/31/12
to std-pr...@isocpp.org
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 preferring

template<typename T>
Foo::Foo(T&& foo)

over

Foo::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.

-- 

 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com(847) 691-1404

Nicol Bolas

unread,
Dec 31, 2012, 4:45:29 PM12/31/12
to std-pr...@isocpp.org


On Monday, December 31, 2012 1:15:05 PM UTC-8, Nevin ":-)" Liber wrote:
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.

Do you use uniform initialization now? If not, why not? If you don't, it's generally for one of three reasons: your compiler of choice doesn't support it, the initializer_list preference makes it impossible to always get the constructor call you want, or you just don't feel like it. I can't do anything about the first or third, but this proposal aims to solve the second. That's important.
 
It doesn't address all the problems people have now.  Another "gotcha" is preferring

template<typename T>
Foo::Foo(T&& foo)

over

Foo::Foo(Foo const&)

for non-const references to Foo objects.

That is completely orthogonal to uniform initialization. This isn't a "resolve everything about initialization" issue; this is a "let's fix this one particular problem" issue. Other issues should get their own fixes. Unless you believe that this problem is somehow related to how braced-init-lists do overload resolution, this problem has nothing to do with the one that I'm proposing a fix for.

The problem you're talking about is not a uniform initialization problem. Initialization is uniform with regard to non-const references to Foo; it will always pick the same constructor. It simply doesn't pick the one you wanted. That's not a problem with using braced-init-lists; that's a problem with overload resolution rules in general. If you want to propose a fix for that, feel free, but that fix will be completely orthogonal to this one.
 
Given our past experience, I have very little confidence that we will now solve the problem of uniform initialization once and for all.

We don't need to solve the problem once and for all; we just need to make things better.
 

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).

Yes, but that is orthogonal to the issue of making braced-init-lists call non-initializer_list constructors. I know; I actually wrote up a unified proposal that included both of these. I'll attach it to this post, but I abandoned the unified proposal because of the other issue: it doesn't resolve the problem in general. It only solves it for the standard library, so everyone has to manually implement the solution, which is not something that can be done automatically and requires great care.

A problem introduced by a language feature should be solved by a language feature. Forcing everyone to implement a workaround is not a solution.
 
 

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.

Which forces you to do this:

std::vector<int> v{1, std::vector<int>::value_init};

As far as I'm concerned, that's unacceptably ugly. `std::piecewise_construct_t` isn't part of std::pair, for obvious reasons. So why should `value_init` or `default_init` be part of it?

Improved Container Initialization.html

Nikolay Ivchenkov

unread,
Jan 1, 2013, 11:55:34 AM1/1/13
to std-pr...@isocpp.org
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.

In a perfect world, we would always use uniform initialization and never use direct constructor syntax.

In a perfect world uniform initialization / list-initialization would never exist. In a perfect world braced-init-list would be a normal expression and we might access its items as shown in the end this message.

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.

I see your "problem" from the other side: why should I ever want to use

   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.

If we want to get 20 value-initialized elements, then

   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.

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).

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?
 
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" };

1) { 1, std::move(s), "string" } is a prvalue of type braced_init_list<int, std::string, const char (&)[7]>

    template <class... _Types>
        class braced_init_list
    {
    public:
        braced_init_list(_Types &&...params) :
            __items(std::forward<_Types>(params)...) {}
        braced_init_list(braced_init_list const &) = delete; // non-copyable & non-movable
    private:
        __tuple<_Types &&> __items;
    };

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.

6) Items of a braced-init-list object can be accessed through std::get:

    template <class... _Items>
        vector(braced_init_list<_Items...> &&__items)
            requires(std::is_constructible<value_type, _Items>()...)
    {
        reserve(sizeof...(_Items));
        __size = sizeof...(_Items);
        auto *__p = data();
        (__create_item_at(__p, std::get<enum(_Items)>(__items)), ++__p) ...;
        // enum(_Items) expands to 0, 1, 2, ..., (sizeof...(_Items) - 1)
    }

    explicit vector(size_type);

Examples:

    std::vector<int> v1(20); // should create 20 value-initialized elements
    std::vector<int> v2{20}; // should be equivalent to std::vector<int> v2 = 20; (ill-formed)
    std::vector<int> v3({20}); // should create one element initialized with value 20
    std::vector<int> v4 = {20}; // should create one element initialized with value 20
    std::vector<int> v5{10, 20}; // should be ill-formed
    std::vector<int> v6({10, 20}); // should create two elements initialized with values 10 and 20
    std::vector<int> v7 = {10, 20}; // should create two elements initialized with values 10 and 20

    void f(std::vector<int> const &v);
    std::vector<int> g(bool b)
    {
        f({20}); // should create one element initialized with value 20

        std::vector<int>{}; // should create value-initialized container
        std::vector<int>{20}; // should be ill-formed copy-initialization of std::vector<int> with 20
        std::vector<int>({20}); // should create one element initialized with value 20

        if (b)
            return {20}; // should create one element initialized with value 20
        return explicit(20); // should create 20 value-initialized elements
    }

    template <class... _Types>
        class tuple
    {
        explicit tuple(_Types const &...);
        template <class... _UTypes>
            explicit tuple(_UTypes &&...);

        // non-explicit
        template <class... _UTypes>
            tuple(braced_init_list<_Items...> &&__items)
                requires(std::is_constructible<_Types, _Items>()...) :
                    tuple(std::get<enum(_Items)>(__items)...) {}
        ....
    };

    struct X {};

    void f1(tuple<int, int> const &);
    void f2(vector<X> const &);

    void f3(braced_init_list<char, char, char> &&li)
    {
        f1({0, 0}); // should be well-formed (in contrast to very strange C++11 behavior)
        f2({10, X()}); // should be ill-formed (in contrast to very strange C++11 behavior)

        char arr1[] = li; // should be well-formed
        char arr2[3] = li; // should be well-formed

        struct A
        {
            char arr[4];
        } a = {li}; // should be well-formed

        char (arr3[]){'a', 'b'}; // should be ill-formed
        char (arr4[])({'a', 'b'}); // should be well-formed
    }

It's too late to do right things now, though.

Nicol Bolas

unread,
Jan 1, 2013, 4:30:50 PM1/1/13
to std-pr...@isocpp.org
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:

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.

But they're not complex; the rules are quite simple. The only complex part is the whole initializer_list issue. Which again, this is intended to solve.

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.

I see your "problem" from the other side: why should I ever want to use

   std::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).
 

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, then

   std::vector<int> v(20);

 does exactly what we want.

That's not using uniform initialization syntax. The point of which is to always use {}; just read Stroustrup's C++ tour on the website, where he advocates always using {}.

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).

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.

This code can be rewritten as follows:
 
    template<typename T>
    T Process()
    {
        T t{};
        // do stuff with t;
        return t;
    }

Yes, but then it wouldn't be an example of the problem. It's an example, something that demonstrates the issue.
 

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?

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.

2) direct-initialize objects returned by value from functions,
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?
 
Alternative design might provide all these capabilities:

    std::string s = "text";
    auto &&li = { 1, std::move(s), "string" };

What you want is something we already have. It's called a tuple. You can construct them from arbitrary arguments via make_tuple. If you want to forward things as a tuple, you use forward_as_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.

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.

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? What's to be gained by it at all?

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.

It's too late to do right things now, though.

Thank goodness, because this is as far from "right things" as it gets.
Message has been deleted

Nikolay Ivchenkov

unread,
Jan 2, 2013, 10:46:16 AM1/2/13
to std-pr...@isocpp.org
On Wednesday, January 2, 2013 1:30:50 AM UTC+4, Nicol Bolas wrote:
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.

IMO, they are neither simple nor intuitive, especially with regard to overload resolution.
 
I see your "problem" from the other side: why should I ever want to use

   std::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).

That's too weak argument, I can just replace int() with int{} or 0. Note that when you want to pass a value-initialized temporary as a template argument, you can't use int() anyway, because it would be treated as a function type.

The downside of "almighty" initialization is weak compile-time checking, this is much more important factor for me:

    int n = 20;
    int *p = &n;
    std::vector<int *> v{p};

Suppose that here I wanted to create *p value-initialized pointers, but I forgot to dereference p. The mistake will not be diagnosed.

The point of which is to always use {}; just read Stroustrup's C++ tour on the website, where he advocates always using {}.

I don't see point there, his argumentation is neither convincing nor even plausible for me.

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

Actually, such "almighty" initialization is unsafe again:

    #include <iostream>
    #include <vector>

    int main()
    {
        for (auto &x : std::vector<int *>{{20}})
            std::cout << x << std::endl;
    }

Here the container will have 20 value-initialized pointers, while I would prefer a compile-time error.
 
There's no way to do the opposite: guarantee that you're calling the constructor.

Well, something like this

    explicit<T>(expr_list) // expression of type T

that would perform only direct-initialization exactly as in

    T t(expr_list);

and (unlike to function-style type conversions) would be unable to perform additional conversions that can be done via static_cast, reinterpret_cast and const_cast, could solve the problem.

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.

There is an open core issue about this: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1467
Probably, the rules will be changed according to the suggestion published there.
 
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.

So, if we don't have a convenient syntax for ()-style direct-initialization of returned objects, then instead of introducing such a useful thing into the language we need to invent a different kind initialization? Sorry, I don't see the point.

What is the conceptual difference between copy-initialization and direct-initialization? Copy-initialization is used for relatively safe initialization of one entity by other entity with similar semantics and value. Direct-initialization is used in the rest of cases. When items of one tuple-like entity are copy-initialized by the respective items of another tuple-like entity, the sequence of such initializations can reasonably be treated as a copy-initialization:

    std::vector<int> v = {1, 2, 3};

In C++03 brace-enclosed initializer lists were used as tuple-like entities for initialization of aggregates which can also be interpreted as tuple-like entities. In C++11 we can implicitly (and suddenly) convert a tuple-like entity to an object with entirely different semantics.

()-style direct-initialization gives me ability to express the intention that comma-separated items should be interpreted as constructor's arguments and _nothing else_. On the other hand, when I initialize an object with an std::tuple object

    X x1 = std::make_tuple(1, 2, 3);
    X x2(std::make_tuple(1, 2, 3));

I definitely don't expect that items of the tuple will be interpreted as arguments for the object's constructor. And I honestly don't understand why braced-init-lists should behave differently.

Could you tell how the following string initialization is performed and how it is supposed to be done?

    std::pair<std::int8_t, std::int8_t> line_prefix(std::int8_t indent);

    std::string gen_prefix_str(std::int8_t indent)
    {
        auto &&prefix = line_prefix(indent);
        return { prefix.first, prefix.second };
    }
 
 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.

Good question. The short answer is: because I prefer to have self-documenting code and good compile-time diagnostics. Copy-initialization of an object tells to everyone that the initializer and the object being initialized are value-related entities and implicit conversion from the source value to the destination value is most likely safe enough. When a copy-initialization causes a compilation error, a similar well-formed direct-initialization may have either intended or unintended behavior, so the code should be reviewed.
 
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.

That's no so.
 
Granted, they reneged on that by making copy-list-initialization not use explicit constructors, but there's a point to that difference.

I don't see a point in that difference. We already have millions of classes (BTW, some of them are part of the C+11 standard library) with non-explicit constructors which have multiple parameters that are not aimed to initialize items of a tuple-like object, so copy-list-initialization would work when it is meaningless according to the original conception of copy-initialization.
 
3) move items of the initializer-list,

Of course not. Those items may be statically allocated.

That's not a convincing argument against movability. Moreover, both options could coexist:

    template <class... Items>
        void f(braced_init_list<Items...> &&li); // 1
    template <class... Items>
        void f(braced_init_list<Items...> const &&li); // 2

    void g()
    {
        f({1, 2}); // calls 1
        f({1, 2}c); // calls 2
    }
 
4) handle items of unrelated types,
5) forward initializer-lists.

... What does that have to do with anything being discussed here?

The intention was to compare my list of desirable capabilities with those we have now and those you are proposing here.
 
What you want is something we already have. It's called a tuple.

And I already use it :-) However, tuple is not so convenient and powerful as braced-initialized-list might be.
 
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.

Temporary objects created in a context of a braced-init-list may obey special rules (so the second statement in the clause 3 would be satisfied). I don't see problems here.

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?

Currently for several contexts we don't have a syntax to express copy-initialization semantics. {} seems to be convenient syntax.
 
The entire point of "uniform initialization" is that it is uniformly used to initialize everything.

Is this an end in itself? What is the point in similarity between code with essentially different semantics? Maybe the goal is a code obfuscation? I don't see any other explanation.
 
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.

I can implement two versions of a generalized function: for direct-initialization and for copy-initialization.

    #define FORWARD(x) static_cast<decltype(x)>(x)

    template <class T, class... Params>
        std::unique_ptr<T> make_unique(Params &&... params)
    {
        return explicit(::new T(FORWARD(params)...));
    }

    template <class T, class U>
        std::unique_ptr<T> make_unique_value(U &&param)
    {
        return explicit(::new T{FORWARD(param)});
    }

    int main()
    {
        auto p1 = make_unique<std::vector<int>>({20});
        // creates a container with 1 element initialized with value 20

        auto p2 = make_unique<std::vector<int>>(20);
        // creates a container with 20 value-initialized elements

        auto p3 = make_unique_value<std::vector<int>>({20});
        // creates a container with 1 element initialized with value 20

        auto p4 = make_unique_value<std::vector<int>>(20);
        // compile-time error
    }
 
They provide full coverage of all possible variants of initialization. Try to implement such generalized makers by means of {}-like "uniform initialization".

Nikolay Ivchenkov

unread,
Jan 2, 2013, 10:53:08 AM1/2/13
to std-pr...@isocpp.org
[Reposting truncated part of the message]

Nicol Bolas

unread,
Jan 2, 2013, 2:41:37 PM1/2/13
to std-pr...@isocpp.org
It's clear at this point that you're getting pretty far off topic now.

This proposal is about fixing a feature according to its design. That is, I see a deficiency in an implementation of a design. I believe my augmentation, as proposed here, would help improve that implementation and thus come closer to filling the actual designed purpose of the feature.

You're saying that the design itself, the feature, is fundamentally wrong-headed.

These are two different discussions. And whether your idea's on enforcing the distinction between direct and copy initialization has merit or not, that's not what this discussion is about.

This is about making uniform initialization work more like it's trying to. Not about saying that the very idea of having a single, uniform method of initialization is wrong.

Nikolay Ivchenkov

unread,
Jan 3, 2013, 5:40:40 AM1/3/13
to std-pr...@isocpp.org
On Wednesday, January 2, 2013 11:41:37 PM UTC+4, Nicol Bolas wrote:

You're saying that the design itself, the feature, is fundamentally wrong-headed.

Exactly. Moreover, I think that list-initialization in its current state should be deprecated (with exception for a few use cases) as soon as possible. And from my point of view, what you are suggesting here is to go further in the wrong direction. I don't want to learn more irregular and pointless rules and read more cryptic code written by others. That's why I here.
 
This is about making uniform initialization work more like it's trying to.

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?

Nicol Bolas

unread,
Jan 3, 2013, 6:17:11 AM1/3/13
to std-pr...@isocpp.org

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.

Nikolay Ivchenkov

unread,
Jan 4, 2013, 3:04:51 PM1/4/13
to std-pr...@isocpp.org
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

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.
 
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.

I think that it's still possible to introduce initializer lists with expression semantics:


    template <class T, class... Params>
        std::unique_ptr<T> make_unique(Params &&... params)
            { return : (new T(FORWARD(params)...)); }
            // return : (arg_list) is another possible extension for direct-initialization

    struct A
    {
        int x, y;
    };

    int main()
    {
        auto p1 = make_unique<std::vector<int>>(10, 1);

        // would create a container with 10 elements initialized with value 1

        auto p2 = make_unique<std::vector<int>>(~{10, 1}); // ~{10, 1} is a prvalue
        // would create a container with 2 elements initialized with values 10 and 1

        auto p3 = make_unique<A>(~{1, 2});
        // would create an object of type A where x = 1 and y = 2

        auto p4 = make_unique<A>(1, 2);
        // would be an error
    }

Nicol Bolas

unread,
Jan 4, 2013, 3:27:02 PM1/4/13
to std-pr...@isocpp.org


On Friday, January 4, 2013 12:04:51 PM UTC-8, Nikolay Ivchenkov wrote:
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

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.

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.

Allow me to reiterate:

Take that elsewhere.

This thread is about resolving a specific problem with uniform initialization. If you don't like the uniformity of uniform initialization, that's your prerogative. But this isn't the place to talk about that. This thread is about making uniform initialization work more like it is designed to, not redesigning it.

Nikolay Ivchenkov

unread,
Jan 4, 2013, 5:39:50 PM1/4/13
to std-pr...@isocpp.org
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>);
    };

Nicol Bolas

unread,
Jan 4, 2013, 6:43:54 PM1/4/13
to std-pr...@isocpp.org


On Friday, January 4, 2013 2:39:50 PM UTC-8, Nikolay Ivchenkov wrote:
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.

When? If an object has a constructor, then it is not an aggregate. Therefore, aggregate initialization is not possible. So for any particular T, you will either use aggregate initialization or call some form of constructor. My proposal is quite clear on this: the only difference between {:} and {} is which constructors take precedence; they behave exactly the same otherwise. There is no conflict.

The only time when it is unclear exactly what {} initialization will do is when choosing which constructors to call. In all other cases, it is completely unambiguous.

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.

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.

Neither such option interferes in any way with this proposal. Currently, a braced-init-list is not deduced at all, and my suggestion changes nothing about that. If the committee later wants to add some rules for braced-init-list deduction, my proposal won't change that determination one way or the 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>);
    };

You keep trying to turn this into a general discussion about uniform initialization and such. I want to keep it focused on the problem I'm trying to solve: making uniform initialization more reasonably usable by fixing one of the biggest downsides of its use: the fact that it prefers initializer_list constructors over the others. That's all.

Are there other non-uniformities? Yes. Are there other places where fixes could happen? Yes. But this is not a suggestion to fix every problem with uniform initialization. This is not a thread to discuss every possible problem with uniform initialization. It's a proposal to fix one specific problem, to make uniform initialization work better. Stop trying to expand it into something too big to go into C++14. If you want to talk about some kind of perfect forwarding for uniform initialization, fine; start a thread on that.

But please stop derailing this thread with issues that have nothing to do with the proposal in question.

Jeffrey Yasskin

unread,
Jan 4, 2013, 7:04:10 PM1/4/13
to std-pr...@isocpp.org
I haven't read the whole thread. Sorry if some of this has been
covered elsewhere.

On Sun, Dec 30, 2012 at 1:49 PM, Nicol Bolas <jmck...@gmail.com> wrote:
> ...
> 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};
> }

When you want to call an algorithmic constructor (i.e. one where the
constructor arguments don't represent the values placed into the
constructed object), use the non-brace format, "std::vector<T>(20)".
Now that we have move constructors and common NRVO support, I would
name that constructor something like
"std::vector<T>::make_repeated(20)", but I wouldn't want to push for a
redundant constructor now.

> 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.

Interestingly, libc++ uses SFINAE to exclude non-callable parameters
from the std::function constructor, so this bug can't happen there.
That may violate the standard, which uses Requires instead of "shall
not participate in overload resolution".

> 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.
>
> ...
>
> 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.
>
> 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.

To make construction use more uniform syntax than the two we have, you
want to add a third syntax to construct things? How does that make
sense?

> ...
> 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.

> ...

Jeffrey

Nicol Bolas

unread,
Jan 4, 2013, 8:50:49 PM1/4/13
to std-pr...@isocpp.org


On Friday, January 4, 2013 4:04:10 PM UTC-8, Jeffrey Yasskin wrote:
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.

That's uniform initialization not being uniform, then, which is exactly the problem this is trying to solve. If you can't use {} everywhere, if you have to use () in some cases, then the syntax needs fixing.


Because it's the only possible way of doing it without a breaking change? Also, it's a minor variation of the syntax; it's hardly new. It's a slight bending of the rules.
 

> ...
> 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.

emplace isn't initializing; it's a function call. The call may happen to forward its arguments to a constructor, but it's still a different circumstance. Solving this requires a mechanism to forward a braced-init-list elsewhere. And that's a whole different can of worms that this proposal isn't trying to solve.

Jeffrey Yasskin

unread,
Jan 4, 2013, 9:22:53 PM1/4/13
to std-pr...@isocpp.org
On Sat, Jan 5, 2013 at 1:50 AM, Nicol Bolas <jmck...@gmail.com> wrote:
> On Friday, January 4, 2013 4:04:10 PM UTC-8, Jeffrey Yasskin wrote:
>> 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.
>
>
> That's uniform initialization not being uniform, then, which is exactly the
> problem this is trying to solve. If you can't use {} everywhere, if you have
> to use () in some cases, then the syntax needs fixing.
>
>> To make construction use more uniform syntax than the two we have, you
>> want to add a third syntax to construct things? How does that make
>> sense?
>
>
> Because it's the only possible way of doing it without a breaking change?
> Also, it's a minor variation of the syntax; it's hardly new. It's a slight
> bending of the rules.

To misquote you from above, "If you can't use {^} everywhere, if you
have to use {} in some cases, then the syntax needs fixing." Note that
I couldn't use {^20} to initialize a std::vector<int> holding just the
element "20", so you can't use {^} everywhere, so your proposal
doesn't solve the problem you set out to solve.

Jeffrey

Nicol Bolas

unread,
Jan 4, 2013, 9:59:38 PM1/4/13
to std-pr...@isocpp.org

Except that you can:

std::vector<int>{:{20}};

See?

Nicol Bolas

unread,
Jan 4, 2013, 10:31:23 PM1/4/13
to std-pr...@isocpp.org

As I think about this, I came to realize that this isn't quite sufficient. I realize that the idea should be this: one should be able to make reasonable predictions about what a braced-init-list could call without knowing the type that it's initializing.

So if I'm in a template context, and I have some type T, and I want to return a newly-constructed T using some parameters, I specifically want to call a constructor. However, if T is an aggregate, it should still work; I want to preserve aggregate initialization, while still calling a constructor if T is not an aggregate.

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:

  • Initializer-list constructors only.
  • Non-init-list constructors only.
  • Initializer-list, then others (the current behavior).
  • Non-init-list constructors, then init-list constructors.
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. So I propose the following. We allow the user to choose which to use based on a "special identifier" (ie: not a keyword).

{<special identifier>: ... }

The possible identifiers are:
  • "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.

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.

I like this syntax very much. It's compact, clear, won't interfere with anything else, and most important of all, gives the user the freedom to choose how all ambiguities of construction are resolved.

Nicol Bolas

unread,
Jan 4, 2013, 11:16:32 PM1/4/13
to std-pr...@isocpp.org
OK, I've updated the proposal with the new {<special identifier>: } syntax.

Introduction

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 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.

Design Overview

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:

  1. Call a non-initializer_list constructor.

  2. Call an initializer_list constructor.

  3. Call a non-initializer_list constructor where applicable; if none fit, call an initializer_list constructor.

  4. 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:

  1. c: Call a non-initializer_list constructor.

  2. l: Call an initializer_list constructor.

  3. cl: Call a non-initializer_list constructor where applicable; if none fit, call an initializer_list constructor.

    Note

    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.

  4. lc: Call an initializer_list constructor where applicable; if none fit, call a non-initializer_list constructor. Equivalent to {}

Impact on the Standard

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.

Design Decisions

What follows are some alternatives and discarded designs, and the reasons for not choosing them.

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.

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.

Binary Choice

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.

Change the Original

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.

Possible Addendum

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.

Technical Specifications

Acknowledgements

  • 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.

References


Towards More Uniform Initialization.html

Nikolay Ivchenkov

unread,
Jan 5, 2013, 8:52:03 AM1/5/13
to std-pr...@isocpp.org
On Saturday, January 5, 2013 3:43:54 AM UTC+4, Nicol Bolas wrote:

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.

Aggregates may have copy/move constructors. The type of an initializer expression may have a conversion function that would make it possible to implicitly convert the initializer to the type of the object being initialized: http://liveworkspace.org/code/2snMf6$0 (as I said before, this is an exotic case).
 
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:

  • Initializer-list constructors only.
  • Non-init-list constructors only.
  • Initializer-list, then others (the current behavior).
  • Non-init-list constructors, then init-list constructors.
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.

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?

Beman Dawes

unread,
Jan 5, 2013, 9:19:16 AM1/5/13
to std-pr...@isocpp.org
On Fri, Jan 4, 2013 at 11:16 PM, Nicol Bolas <jmck...@gmail.com> wrote:
> OK, I've updated the proposal with the new {<special identifier>: } syntax.

You might want to consider changing:

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.

to:

The intent of the proposal is to break no existing code. Regular
braced-init-lists are intended to have the same behavior as in C++11,
and the new grammar would not have been legal in C++11.

Assuming that's what you meant, of course:-)

You might also want to add a section "Implementation experience" that
just says "None yet." While that might seem obvious, it signals that
you are aware that a proposal of this magnitude would have to be
implemented at a later stage of the process.

Cheers,

--Beman

Nicol Bolas

unread,
Jan 5, 2013, 2:22:35 PM1/5/13
to std-pr...@isocpp.org


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:
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.

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. You know, exactly what this proposal is trying to solve with {}.

Also, it would still allow the most vexing parse. As well as prevent basic syntax like `Type t();` from working.
 
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:

  • Initializer-list constructors only.
  • Non-init-list constructors only.
  • Initializer-list, then others (the current behavior).
  • Non-init-list constructors, then init-list constructors.
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.

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. As stated above. You personally may not want to do 3 or 4, but the "not you" demographic certainly does sometimes.

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?

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?


The presence of an initializer_list constructor would be part of the concept that the template places on its types. Just like std::vector requires that T be at least MoveConstructible. How an object can be used is defined by the template definition, not the user. Initialization is part of how an object is to be used.

If a template says, "I expect to be able to call {lc: ...} to initialize the type you pass for some set of values", then you must provide a type that will be able to be initialized with that braced-init-list. If you don't, that's an error on your part. You don't get to decide how a template uses your type; the template decides that. Initialization is no different.

The moment you hand initialization duties off to someone else, you give up the right to decide exactly how to initialize it.

Nikolay Ivchenkov

unread,
Jan 5, 2013, 5:04:26 PM1/5/13
to std-pr...@isocpp.org
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:
  • Initializer-list constructors only.
  • Non-init-list constructors only.
  • Initializer-list, then others (the current behavior).
  • Non-init-list constructors, then init-list constructors.
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.

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?

Nicol Bolas

unread,
Jan 5, 2013, 6:02:01 PM1/5/13
to std-pr...@isocpp.org


On Saturday, January 5, 2013 2:04:26 PM UTC-8, Nikolay Ivchenkov wrote:
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.

Except for when it looks like a function definition. And you must explicitly use a typename when using it. 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.

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:
  • Initializer-list constructors only.
  • Non-init-list constructors only.
  • Initializer-list, then others (the current behavior).
  • Non-init-list constructors, then init-list constructors.
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.

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 don't see what you're getting at here.

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.

How not? What the compiler does is very much a part of the goals of "ordinary programmers". 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. The point of this feature is to allow programmers to use {} more effectively by giving them control over which constructors will be considered.

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.

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?

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?

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.

As I stated in the proposal, std::allocator_traits::construct could be redefined in terms of {c:} instead of non-list initialization, so that users can use aggregates with them.

Nikolay Ivchenkov

unread,
Jan 6, 2013, 3:03:34 PM1/6/13
to std-pr...@isocpp.org
On Sunday, January 6, 2013 3:02:01 AM UTC+4, Nicol Bolas wrote:

We already use ()-wise syntax for direct-initialization.

Except for when it looks like a function definition.

The problem can be solved in two ways: by new initialization syntax or by new declaration syntax:

    simple-declaration:
        ....
        decl-specifier-seq opt <type-id> simple-init-declarator-list

    simple-init-declarator-list
        simple-init-declarator
        simple-init-declarator-list, simple-init-declarator

    simple-init-declarator:
        declarator-id initializer opt

Example:

    int main()
    {
        <char[2]> arr(); // value-initialized object of type char[2]
    }
 
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?


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

A goal and a way in which the goal is (or can be) reached are two different things. If the way becomes a goal, that's something irrational.

Good readability of sources can be considered as an intermediate goal. The readability represents the difficulty of deducing the correct interpretation, which depends on complexity of the language rules.
 
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})
    }
 
Are there any other tasks for which {lc: ...} would be useful?

Nicol Bolas

unread,
Jan 6, 2013, 4:30:39 PM1/6/13
to std-pr...@isocpp.org


On Sunday, January 6, 2013 12:03:34 PM UTC-8, Nikolay Ivchenkov wrote:
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.

That's wonderful; do it elsewhere. Please stop derailing this thread with counterproposals; I'm trying to refine this proposal into something that improves uniform initialization. Stop turning this into a "here's a completely different initialization syntax that has completely different semantics that are in no way shape or form uniform that we should adopt instead" thread.
 
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?

What do you mean by "forwarded"? It's an initializer_list. If {l:} is not used in aggregate initialization, then we could have it build an initializer_list when used in template deduction scenarios. And you can't use initializer_lists to initialize an aggregate.
 
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?

x1 will do exactly what it says. Since the members of {c:} match a constructor exactly, it will call that constructor. That the parameter to this constructor just so happens to be of type `std::initializer_list` is irrelevant. This works exactly as it would if you used {std::initializer_list<int>{20}} with the current syntax. This is defined per the rules of 13.3.1.7, just as if you'd passed an initializer_list as an argument to the constructor.

Remember: the 13.3.1.7 provisions about std::initializer_list constructors are about considering the members of the braced-init-list to be the members of an initailizer_list. That one of the members might itself be an initializer_list is irrelevant to that point.

x2 would follow the current overload rules. {20} could be used to initialize both `int` and `std::initializer_list<int>` equally. I'm fairly sure this would be considered an ambiguous call, since both conversions are equally good. If int{20} is not considered a conversion, then it may pick the `int` constructor, as it is an exact match. But I'd have to look at 13.3.3.1 and 2 in more detail to know for sure. More importantly, the behavior will be consistent with how {{20}} currently behaves.
x3 is also very clear. A `std::initializer_list<int>` has no constructor that takes a single integer, so an implicit conversion from {c:20} to that is not possible. Therefore, it will call the `int` constructor of `X`.

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.
 

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?

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);

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.

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

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. I simply want to pass these two parameters to it to initialize it. So do that.

Also, your code doesn't allow aggregate initialization in cases where `Aggregate` isn't just a bunch of `ints`. You know, if it has multiple types. Indeed, I don't see how p1 works at all, considering that you cannot initialize an aggregate from an initializer_list.

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. If you let someone else initialize your objects for you, then they will decide on the semantics of that initialization, not you. All you can provide are parameters.

My goal is to better allow the function actually doing the initialization to express their intent. Whether someone can forward their intent to another function is an orthogonal issue.

 
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. Therefore, unless you can prove that it is of no value to the user, I will operate under the assumption that he knows what he's talking about.

Personally, I don't much care for it; as I said in the proposal, I would have preferred if you were forced to add the extra {} if you wanted initializer_list initialization. I'm simply making the feature better in whatever way I can, not throwing it away and creating something new.

Nikolay Ivchenkov

unread,
Jan 7, 2013, 4:07:29 AM1/7/13
to std-pr...@isocpp.org
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}).
 
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).

See 8.5.4/7/3:

    A narrowing conversion is an implicit conversion
        [...]
        — from an integer type or unscoped enumeration type to a floating-point type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted back to the original type

In the example n is not a constant expression, so the definition

    double d1{n};

invokes a narrowing conversion.

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) 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.

And readers have to spend a lot of time in order to find out what such a code is supposed to do. Moreover, if we have something like this


    struct X
    {
        X(int);
        X(std::initializer_list<int>);
    };

we can't later add a constructor with additional parameter of type int

    X(int, int);

so that this change would not imply potential breakage of a client's code.

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 will change your opinion when you will have to debug or modify such a code.
 
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.
 
and therefore works against the whole point of uniform initialization.

against the whole point which is purely abstract :-)
 
I, as the user, should neither know nor care if `Aggregate` or `Type` has a constructor or is an aggregate.

...unless you do care about programmers who will try to understand your code.
 
Also, your code doesn't allow aggregate initialization in cases where `Aggregate` isn't just a bunch of `ints`.

~{items} is supposed to reflect types and value categories of all its items and cover nesting:

    ~{ ~{"text", nullptr}, 1 }

and

    ~{ {"text", nullptr}, 1 }

would have the type std::braced_init_list<std::braced_init_list<const char (&)[5], std::nullptr_t>, int>
 
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.

And I don't want to forward the mechanics of braced-init-lists, because it's too complicated and irregular. It would be enough to be able to initialize aggregates and objects of types std::initializer_list<ItemType> or std::braced_init_list<ItemTypes...> from forwarded lists.

 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. We have to use this obscenity sometimes only because the language does not provide any alternative ways to get a desirable behavior using a less obscure notation.

Nicol Bolas

unread,
Jan 7, 2013, 6:52:33 AM1/7/13
to std-pr...@isocpp.org


On Monday, January 7, 2013 1:07:29 AM UTC-8, Nikolay Ivchenkov wrote:
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.

We'll see about the latter; I very much intend to make it a formal proposal, with a real number and everything. Though I can't attend a meeting in the near future to properly defend it. As for the former, it certainly won't be approved if you don't actually make a proposal. If you think that it's something that should go into C++, that will make the language better, then you should have a go at it.

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}).

There's no way to guarantee that. The best you can do is what I said: allow {l:} to be template deduced as some form of std::initailizer_list of some type, where possible. The problem is that you'd have to guess the type, which can lead to mismatches when it comes time to use it with the actual type. So it's not exactly "forwarding".

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

Fair enough. Though some might say that this is an improvement, that you should be more explicit about the types of literals.

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) than vv.emplace_back(~{20, 30, 40, 50}).

Yes, and some might say that memorizing different initialization rules for different types of objects is a bad thing to. Some might say that having to initialize types with constructors differently from those without is harder to read too.

Some might say a lot of things. That doesn't make them right or not.

You asked how that would help express their intent. I explained how. The fact that you don't like that reasoning doesn't make it any less valid to those who do like it. Some people want to be able to do that, and I don't believe there is a good reason to deny them.

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),

No, it can't. It can only be initialized by an initializer_list if the aggregate is a sequence of items of the same type. Remember: std::initializer_list can only contain items of the same type.

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.

Please read the thread title. It's called, "RFC: Towards More Uniform Initialization Proposal". The thread is about a fix to uniform initialization syntax to make it more uniform and more uniformly useable.

Your non-uniform commentary and suggestions are unhelpful. Please take your discussion of non-uniform initialization syntax elsewhere. Your continued thread derailment is starting to border on trolling at this point, since I have repeatedly ask you to stop bringing these things up.

 Are there any other tasks for which {lc: ...} would be useful?

I was going to ask about {cl: ...}, but I made a typo :-)

As I pointed out in the proposal, it's about orthogonality. You need {lc:} because that's the default behavior, which can't be changed. Adding {cl:} makes sense because it's expected given that you have {lc:}. It would seem like a missing feature otherwise.

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.

You're simply reiterating the purpose of this proposal: to fix the small issues in what is otherwise a great initialization syntax, thus allowing the user to express "every desirable way of initialization".

Fernando Pelliccioni

unread,
Jan 7, 2013, 8:15:29 AM1/7/13
to std-pr...@isocpp.org



On Sun, Dec 30, 2012 at 6:49 PM, Nicol Bolas <jmck...@gmail.com> wrote:
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.

Introduction

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.

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.

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.

Impact on the Standard

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.

Design Decisions

Bikeshedding

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.

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.

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.

Alternate Designs

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.

Change the Definition

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.

--
 
 
 

Hi Nicolas,

I think your proposal combined with "Named Arguments" (I can not find the paper) could be useful.

I don't know the details of the "Named Arguments" proposal, but, from Bjarne's "The Design and Evolution of C++" Section 6.5.1:
"Roland Hartinger's proposal for keyword arguments, that is, for a
mechanism for specifying arguments by name in a call, was close to
technically perfect. The reason the proposal was withdrawn rather than
accepted is therefore particularly interesting. ..."


Without having analyzed in depth, I imagine something like this:

vector<int> v1{20};             //Creates an array of 1 item.
vector<int> v2{ count := 20 };  //Creates an array of 20 items.
vector<int> v3{20, 30, 40};     //Creates an array of 3 items.


I think this syntax is clearer, less ugly and also gives us an excuse to incorporate "Named Arguments" into the language.

Regards,
Fernando Pelliccioni.
 

Nicol Bolas

unread,
Jan 7, 2013, 4:35:00 PM1/7/13
to std-pr...@isocpp.org

Except that it doesn't really solve the actual problem in question. Observe:

vector<int> v1{20};             //Creates an array of 1 item.

vector
<Type> v1{20};            //Creates an array 20 Types.

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.

Fernando Pelliccioni

unread,
Jan 8, 2013, 12:17:56 AM1/8/13
to std-pr...@isocpp.org
Perfect, like C++11
 
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. 
 
The idea of a template's concept requiring a parameter name is very... disconcerting.


 
I would like to access the original proposal to view it in detail.

Regards,

Nicol Bolas

unread,
Jan 8, 2013, 4:03:16 AM1/8/13
to std-pr...@isocpp.org

How is that "perfect" by any definition? Why should the same initializer do two completely different things to the resulting object? Just because you changed the template's type. That's not exactly "uniform" initialization, is it? Constructor syntax is not ambiguous about what gets called: you call a constructor. So why should uniform initialization syntax be so oddball that it can radically change what it does based on the type being initialized.

As I point out in the proposal, this means that you cannot do `T v1{20};` in a template context. Or even just `std::vector<T> v1{20};` Because you don't know which constructor it will call; it may do the wrong thing from your intentions.

If you can't trust {} syntax, then what purpose does it serve? My proposal is intended to make it more trustworthy by allowing the user to more explicitly specify what they actually want.

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 ?

It's in the proposal. You should familiarize yourself with it, particularly version 2, before commenting further.

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.

The example shows that your grammar doesn't solve the problem. The problem, as stated, is that uniform initialization picks initializer_list constructors first. Because of that, it can hide others.

Your solution has the these problems, as outlined in my example:

1: The user has no way to force the use of initializer_list constructors. It doesn't allow the user to say "I want to use an initializer_list constructor, and if one doesn't match for this type, stop compiling: the user did something wrong." You could do that with constructors: `std::vector<T> v1({10});` will not compile if T is not an `int` or some integer type that is appropriately sized. Uniform initialization should be able to do everything that constructor syntax can (modulo uniformity and ditching narrowing).

2: It forces the use of named argument calling if you want to explicitly call a constructor. Functions in C++ do not have to name their arguments. Furthermore, putting argument names in template concepts is a dubious prospect, as there are certainly no mechanisms to use std::enable_if or any similar construct on them.

3: It breaks aggregate initialization. Therefore, you cannot uniformly use the same syntax to initialize a type without knowing what the type is. Thus, it violates the whole point of uniform initialization.
 
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.

We're not just talking about the one case of initializing a std::vector. We're talking about all initialization of anything.
 
How does your proposal work with aggregate initialization?

Very well. Exactly as it does for uniform initialization, as stated in the proposal. The only thing it changes is the list of constructors that are chosen from, if constructors are being looked at. Aggregates don't have constructors, so they use aggregate initialization rules.
 
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.

And many other languages don't have that concept. Moreover, it's rather off-topic, as it doesn't solve this particular problem, as stated.

Suggesting a giant feature to solve a small problem is not a good plan for success.

Sebastian Gesemann

unread,
Jan 8, 2013, 4:12:40 AM1/8/13
to std-pr...@isocpp.org
On Sun, Dec 30, 2012 at 10:49 PM, Nicol Bolas wrote:
>
> Introduction
> [...]
> In a perfect world, we would always use uniform initialization and
> never use direct constructor syntax.

I'm fine with the way it's currently handled. I'm fine with being able to write

vector<int> foo (20,30); // 20 elements with value 30
vector<int> foo {20,30}; // 2 elements with value 20 and 30

to distinguish between the more specialized direct initialization and
a "logical, aggregate-like" initialization.

> However, despite being called “uniform initialization,” it cannot be
> uniformly used everywhere, due to a critical flaw that prevents its use
> towards these ends.

I don't think this syntax was ever intended to be the only kind of
initialization everybody should use from now on in every situation.

> 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.

Why?

> Motivation and Scope

Ok, convince me.

> 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?

Why not? I find it makes sense. Logically, a vector<int> is an
aggregate of a variable number of ints and the curly braces are just a
way of initializing this "aggregate-style" types. I tend to think of
{} as the kind of initialization which specifies the (logical)
elements of the to-be-initialized object.

> 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.

Sure, there is a syntax:

vector<int> v1(20);

That's what direct initialization is for.

> 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.

So? If you want the vector to contain 20 default constructed elements
use the proper initialization syntax!

> 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.

I find your examples hardly convincing. What problkem is Process<>
supposed to solve? Why don't you just return t?

I think the issue is that you think you are supposed to use {}
everywhere but you're not.

> 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.

> 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 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).

my 2 cents,
Sebastian

Nicol Bolas

unread,
Jan 8, 2013, 4:35:37 AM1/8/13
to std-pr...@isocpp.org

But that's not how the feature is designed. If it's only supposed to be used in cases of aggregate and initializer_list initialization, why does this work:

std::vector<Type> v1{20}; //Creates a vector of 20 default-constructed items.

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..."

It makes the language simpler to use.

> 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.

These two statements seem contradictory. You clearly see what I'm talking about; you just don't accept that it is a problem. There's a difference between saying "You're not being clear" and "I understand what you're saying but I don't buy it".

I don't like it. It does not seem to solve any problems but only cause
more. For example, this is not "perfectly forwardable".

{} 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.
 
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).

As stated in the proposal, the problem is that it only solves it for that type. This is a problem introduced by a language feature. If you try to solve it at the code level, then everyone will have to implement their own, slightly different and completely incompatible solution. It still won't be something you can depend on in template code when the types are unknown.

In short, it doesn't solve the problem. Problems caused by language features need language solutions. Trying to fix language problems with library solutions is why we have the nightmarish `std::enable_if` instead of the much more sane, rational, and powerful `static if`.

Nevin Liber

unread,
Jan 8, 2013, 10:57:16 AM1/8/13
to std-pr...@isocpp.org
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?
--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com(847) 691-1404

Jeffrey Yasskin

unread,
Jan 8, 2013, 11:46:28 AM1/8/13
to std-pr...@isocpp.org
Some types, say, pair<> and tuple<>, have non-initializer-list
constructors taking arguments that represent their contents after
construction. Allowing {} to produce constructor arguments makes
pair<> and tuple<> usable like aggregates. vector<> has a constructor
taking an argument that doesn't represent the contents after
construction but instead describes how to create those contents. There
wasn't a way to prevent vector<>'s constructor from working with list
initialization, so it does, but that doesn't mean allowing it was a
goal. The committee could have added a syntax to mark a constructor as
unusable with list-initialization, but either they didn't think of it
or didn't think it was worth doing. If you want to read the history,
check out the "initializer lists" line in
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2869.html.

-1 on this proposal, agreeing with Nevin. I don't intend to reply further.

Jeffrey

Sebastian Gesemann

unread,
Jan 8, 2013, 12:23:12 PM1/8/13
to std-pr...@isocpp.org
On Tue, Jan 8, 2013 at 10:35 AM, Nicol Bolas wrote:
> On Tuesday, January 8, 2013 1:12:40 AM UTC-8, Sebastian Gesemann wrote:
>>
>> Why not? I find it makes sense. Logically, a vector<int> is an
>> aggregate of a variable number of ints and the curly braces are just a
>> way of initializing this "aggregate-style" types. I tend to think of
>> {} as the kind of initialization which specifies the (logical)
>> elements of the to-be-initialized object.
>
> But that's not how the feature is designed. If it's only supposed to be used
> in cases of aggregate and initializer_list initialization, why does this
> work:
>
> std::vector<Type> v1{20}; //Creates a vector of 20 default-constructed
> items.

I didn't say that {} only works aggregates and for classes with
initializer_list constructor. I am well aware of other classes like
complex<double> where you can initialize objects with curly braces as
well.

What you seem to be annoyed with is the overloading of
std::vector<T>'s constructors and the overload resolution rules.

> 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.

>> 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.
>
> These two statements seem contradictory. You clearly see what I'm talking
> about; you just don't accept that it is a problem. There's a difference
> between saying "You're not being clear" and "I understand what you're saying
> but I don't buy it".

I don't know how else to express it without repeating myself. But I'll
try: The "problem" is that you think {} is supposed to be applicable
everywhere. This is not a C++ language problem. This is you making a
false assumption.

>> I don't like it. It does not seem to solve any problems but only cause
>> more. For example, this is not "perfectly forwardable".
>
> {} 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.

Correct. But if I remember correctly a member of the standardization
committee already hinted at equalizing the deduction rules between
auto and templates so that in a case like

template<class T> void foo(T)
int main() { foo({1,2,3}); }

the compiler will deduce T to be std::initializer_list<int> just like
it already does for auto:

auto bar = {1,2,3};

in which case you will be able to make this work:

template<class T, class...Args>
unique_ptr<T> make_unique(Args&&...args)
{
unique_ptr<T> p (forward<Args>(args)...);
return p;
}

int main()
{
auto p = make_unique<std::vector<int>>(20); // creates a vector
with 20 elements
auto q = make_unique<std::vector<int>>({20}); // creates a
vector containing the value 20.
}

It won't work in all cases and I think it just cannot but it's good
enoug, I think.

>> 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).
>
> As stated in the proposal, the problem is that it only solves it for that
> type. This is a problem introduced by a language feature. If you try to
> solve it at the code level, then everyone will have to implement their own,
> slightly different and completely incompatible solution. It still won't be
> something you can depend on in template code when the types are unknown.

What problem?

Perhaps this is the time where you show us some real code with a real
problem you like solved and not just some "look how {} does not work
like I expected in this case"-examples. ;-)

Cheers!
SG

Nicol Bolas

unread,
Jan 8, 2013, 1:58:46 PM1/8/13
to std-pr...@isocpp.org


On Tuesday, January 8, 2013 9:23:12 AM UTC-8, Sebastian Gesemann wrote:
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.

Really? Bjarne Stroustrup developed uniform initialization. So let's ask him what the designed purpose of it is. From N2532, "Uniform Initialization design choices (Revision 2)":

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.

So yes, the goal very much was something that "can be used everywhere;" it is intended to be able to be used in all cases. So it's not "my problem"; it is very clearly a problem between the intent of uniform initialization and the current version as implemented in C++11.

My proposal is simply trying to move from being "approximated" closer to the "ideal".

If you don't like that goal, fine. But you can't argue that this isn't the goal.

Nicol Bolas

unread,
Jan 8, 2013, 2:14:42 PM1/8/13
to std-pr...@isocpp.org


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 {}.

Since you have to default to (), you'll still get the MVP. The most {} does for you in those cases is that you can use {} to initialize something in those specific cases. For spot fixes. Then again, you could usually wrap the type in parenthesis to achieve the same effect.

Without the ability to freely use {} syntax everywhere, it doesn't really solve the problem.
 
  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.
I'm not sure how these are cryptic. They are all slight variations of the same thing.

And if it's really a problem, we could drop {lc:} and {cl:}. {c:} and {l:} are the main focus: constructors vs. list.


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.

This proposal only has 1 method of initialization, with 4 modifiers on the behavior of it. And the modifiers are pretty obvious.

{} 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?

I don't know how the initialization forwarding problem will be solved, but there's no reason it would need new initialization syntax to be solvable.

In any case, what happened was someone decided to make {lc:} the default for {}, so that they could type as many braces for `std::vector` as for `std::array` and `Type[]`. In effect, someone wanted brace elision for uniform initialization. As stated in the proposal, the ideal solution would have been to make {c:} behavior be the default, thus forcing you to use multiple braces if you wanted a list:

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

That way, you could always access any constructor via uniform initialization. They didn't do that, and it's too late to fix that now. There are no "simpler" solutions to the problem. Therefore, I picked the second best solution: allow the user to state which behavior they want out of a particular braced-init-list.

Nevin Liber

unread,
Jan 8, 2013, 2:42:23 PM1/8/13
to std-pr...@isocpp.org
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.
 
I'm with Jeffery on this one. -1 for the proposal, and barring new information, I probably won't comment on it anymore unless someone presents it in EWG. 

Nicol Bolas

unread,
Jan 8, 2013, 3:01:22 PM1/8/13
to std-pr...@isocpp.org


On Tuesday, January 8, 2013 11:42:23 AM UTC-8, Nevin ":-)" Liber wrote:


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?

Are you telling me that 8.5.4 all of the other language in the standard around braced-init-lists exists for the sole purpose of being able to say "default construct T?"
 
 
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?

I'm not entirely sure about how the rules of overload resolution work in this case, but I think that using S{l:0} will use the integer version, and S{l: 0.0} will use the double-precision version.
 
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.

I'm confused. The standard is quite clear that an empty braced-init-list will result in calling the default constructor, in all cases. The only place where there's a difference is the latter case, and that one makes perfect sense. You're not saying "create an empty `S`". You're saying "Create an `S` that takes one parameter, which is a default constructed argument who's type will be deduced based on the available single-parameter constructors on `S`."

Those are two different things, so they should result in different objects. In uniform initialization, {} doesn't mean "do nothing"; {} aren't lists; they're initializers. It means "initialize an object of a type determined by context."

vattila...@yahoo.co.uk

unread,
Jan 25, 2013, 6:11:15 PM1/25/13
to std-pr...@isocpp.org
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.
 
You could even allow an unnamed argument in this case:
 
vector <int> a {:= 42}; // OK: Pass size.
 
Regards,

Vidar Hasfjord

 

Nicol Bolas

unread,
Jan 25, 2013, 9:59:06 PM1/25/13
to std-pr...@isocpp.org, vattila...@yahoo.co.uk


On Friday, January 25, 2013 3:11:15 PM UTC-8, vattila...@yahoo.co.uk wrote:
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.

I don't like this solution, because it doesn't really solve anything. It forces class designers to have to design around this ambiguity. Which means everyone is going to come up with their own, completely incompatible solutions to a problem.

Language problems should have language solutions, not library solutions. Making everyone implement incompatible changes to their constructor interfaces just because we don't want to make initialization slightly more complex is not the way to go.

Template code with your method is still unable to yoke the benefits of {} syntax, because it can't be sure that {} will do the right thing. You can't initialize aggregates with (), so there's no way to write a template function that could take an aggregate or a non-aggregate class and initialize them with the same syntax. You'd have to do some `static if` or `std::enable_if` stuff to test the type and pick one vs. the other.

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.

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.

And disambiguation in favor of constructors is the most important feature. It's what allows you to always use {} initialization on anything, thus achieving uniform initialization and consistent results.

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.

My gut feeling is that named arguments are a bad idea in a language that doesn't require argument names or even consistent argument names between the function declaration and definition. Let alone issues with function/member pointers. So I don't buy that this "broader context" is something that should be considered, regardless of uniform initialization.

How would you use this in a template concept? How would you use it for initializing some type T, which may or many not have a constructor parameter named `size`? Most important of all, how would this work with aggregates; remember, this is supposed to be uniform initialization.

Nikolay Ivchenkov

unread,
Jan 26, 2013, 3:54:51 AM1/26/13
to std-pr...@isocpp.org
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());

The following code

    std::vector<int *> v({42});
    std::cout << v.size() << std::endl;

will print 42.

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.

Again, the following initialization

    std::vector<int *> a{42,};

is allowed under the current rules.

My gut feeling is that any additional syntax to improve on this issue 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.

vattila...@yahoo.co.uk

unread,
Jan 26, 2013, 2:22:27 PM1/26/13
to std-pr...@isocpp.org
Hi Nicolay,
 
The following code

    std::vector<int *> v({42});
    std::cout << v.size() << std::endl;

will print 42.
 
As it should. There is no apparent ambiguity for vector <int*> {42}. The apparent ambiguity only accurs when the element type is implicitly convertible the type of n-parameter for the vector <T> (size_type n, const value_type&, const allocator_type&) constructor.
 
[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.
 
Your replies was one of the reasons I came to that view. There is obviously room to explore the design space here.
 
Regards,
Vidar Hasfjord
 

vattila...@yahoo.co.uk

unread,
Jan 26, 2013, 2:53:48 PM1/26/13
to std-pr...@isocpp.org, vattila...@yahoo.co.uk
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.

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.

Regards,
Vidar Hasfjord
 

DeadMG

unread,
Jan 26, 2013, 5:16:17 PM1/26/13
to std-pr...@isocpp.org, vattila...@yahoo.co.uk
Hmm. It's easy to define the ideal solution, but more difficult to see how to reach it without breaking existing code. It should be that for std::vector<int> { 43 }, this invokes the constructor, and you should be forced in all situations to use a double brace if you want to uniform-initialize with an initializer-list. Not having this available unless the Committee wants to do a breaking change, which I expect not since some breaks would be silent like the above case, means that we'll have to replace uniform initialization, and I think that if that's going to be the case that we should just go straight for the ideal. But, IMO, the "correct" behaviour is only one- 'c'. Thus, I'd simply ditch the special character and move to something like {^- i.e., fixed uniform initialization. I'd also say that we should simply move to language-level tuple support.

Nicol Bolas

unread,
Jan 26, 2013, 6:01:34 PM1/26/13
to std-pr...@isocpp.org, vattila...@yahoo.co.uk
On Saturday, January 26, 2013 11:53:48 AM UTC-8, vattila...@yahoo.co.uk wrote:
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.

That's putting the cart before the horse. 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 latter is a nice thing to have, but it is only nice if you have a real solution.

Making the ambiguity illegal should never happen without an already approved plan to allow users to resolve the ambiguity. That's leaving things in a worse state than they currently are.

Also, it's a breaking change, thus requires far more careful consideration than purely augmenting changes. My assumption going in is that the standard committee is not going to make a major breaking change like that.

Lastly, if they are willing to make a breaking change, I would much prefer they prevent the ambiguity directly by removing the competition. Let {} mean "constructor with these arguments" for non-aggregates. It will mean having to double-up on braces in some cases to get to the initializer_list constructors, so you don't get brace elision. But it does let you get at all the constructors you want.

If you're going to break the language, then break it in the way that leads to the ideal solution. Don't break it and make the language more complex with := and what not.

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.).

Just on quality of syntax issues, using := here looks horrible. It looks like you're trying to assign something. It's a syntax that only really makes sense when dealing with named arguments. By taking out the argument name, it now just looks really confusing and out of place. Even moreso if := were adopted for named arguments, because then the user expects it to be used for named arguments.

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.

You've managed to make an ugly syntax even uglier by now implying that `:=` means "initialize" in some cases, where in another it means "assign to named argument" where in a third it means "call constructor".

The user now has no idea what := is supposed to even mean at this point. Sometimes it gets wrapped in braces, sometimes it doesn't; sometimes a name comes before it, sometimes it doesn't.

How is this less confusing than `{c: ...}`? At least that you can use everywhere in the same way:

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.

Whereas your syntax would look like this:

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 {:= ...}; }

Oh, and the whole "no need for copy-initialization" part? You may want to read this PDF, as it is the reason the copy-list-initialization was added to uniform initialization. Those reasons don't go away just because you put a `:` before the equals.

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.

If it implicitly added a constructor, it would no longer be an aggregate. I would feel really uncomfortable about compilers automatically creating functions like that for simple classes.
 
> 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.

 
What good is having a solution that you can't reliably use? We can fix the most-vexing-parse with an extra set of parenthesis when it comes up. And narrowing can be lived with.

vattila...@yahoo.co.uk

unread,
Jan 26, 2013, 8:32:24 PM1/26/13
to std-pr...@isocpp.org, vattila...@yahoo.co.uk
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. By dismissing them, you are overstating your case.
 
> Lastly, if they are willing to make a breaking change, I would much prefer they prevent the ambiguity directly by removing the competition. Let {} mean "constructor with these arguments" for non-aggregates.
 
There are different types of breaking changes. A silent break, as the one you are suggesting here, is considered the worst kind by most. Hence I very much disagree with you here. This rule change would silently change vector <int> {20} to mean a vector of 20 default-constructed elements, not a vector of one element (20) as currently is the case. The bug this causes in client code may not even show up in testing.
 
> [Your horribly ugly syntax] means "initialize" in some cases, where in another it means "assign to named argument" where in a third it means "call constructor".
 
Argument binding is initialization, and initialization implies constructor calls, so I don't get your point here, other than that you obviously didn't particularly like going down this path. :-)
 
> How is [your syntax {:= ...}] less confusing than `{c: ...}`?
 
I didn't claim that. I deem them at the same confusion level. My syntax suggestion is just an exploration of the design space in the direction of named arguments. I think that has some attractive features, but it is just brain-storming at this point. But you don't seem particularly keen to explore this path, so I will disengage.
 
> Oh, and the whole "no need for copy-initialization" part? You may want to read this PDF, as it is the reason the copy-list-initialization was added to uniform initialization. Those reasons don't go away just because you put a `:` before the equals.
 
I think you are complicating things here. I showed how my suggested brace-less syntax is a simple syntactic transformation from direct-initialization using braces ("constructing by calling a constructor", as the article you linked calls it). The elision of the braces is not meant to change semantics. If you think it should, that is another matter. Note that the linked article explores the adaptation of initializer-list in a "=" context, where it needs to fit with the existing rules.
 
> If [C++] implicitly added a constructor [to aggregates], it would no longer be an aggregate.
 
This does not have to be the rule. An aggregate already has an implicit default constructor, copy constructor and assignment operator.
 
Note the formal definition from the C++ standard (C++03 8.5.1 §1): An aggregate is an array or a class (clause 9) with no user-declared constructors (12.1), no private or protected non-static data members (clause 11), no base classes (clause 10), and no virtual functions (10.3).
 
> 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.
 
Regards,
Vidar Hasfjord
 

Nicol Bolas

unread,
Jan 26, 2013, 9:51:16 PM1/26/13
to std-pr...@isocpp.org, vattila...@yahoo.co.uk
On Saturday, January 26, 2013 5:32:24 PM UTC-8, vattila...@yahoo.co.uk wrote:
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.

Having a broken feature is bad. It's worse than not having the feature at all. Especially when you've got Bjarne Stroustrup using it everywhere in his book like it's not broken; that's thousands of naive C++ programmers that are going to get hit with the problem.
 
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.

Available, but they don't fix the problem. They fix the problem for std::vector. But they don't fix the problem for <insert some type here>. It's a problem introduced by a language feature; that's the level where it needs to be fixed.

The longer this problem is present, the worse it will get. The more people are going to encounter it, and the more people are going to have problems/malformed code. Eventually, what's going to happen is that people will idiomatically avoid uniform initialization syntax, deeming it worse than regular initialization. That's what the C++ idioms are going to be in 5 years if something isn't done to correct the problem at the language level.

If I can't write `T {20, 30}` in some template code and have some idea of what it will actually do, then there is a problem. My library level solution can't fix that; only a language-level solution can.

> 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.

We currently cannot reliably use uniform initialization syntax everywhere because of these issues. What you propose... would continue to prevent us from reliably using uniform initialization syntax everywhere. Code that does it is no more safe; it just emits a compiler error. Sometimes. That's not progress; that's a lateral move.

Furthermore, by not allowing this "progress", we force the committee to provide a real solution ASAP. This is a legitimate problem that must be addressed. But by removing the simple "solution", we force them to look at real solutions that make uniform initialization actually work.

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.

Just look at this thread. You've got people trying to piggyback named parameters and initialization forwarding and other things into this discussion. As though these entirely orthogonal issues had anything to do with fixing this specific problem. Everyone is going to try to stick their own spin on it, with their own specific issues and ideas about it. People who think that uniform initialization is itself completely wrongheaded will try to shoot it down or otherwise sabotage it. People who want named parameters will make the fix contingent on them. And so forth. Everyone's little pet issues will come out, and they'll try to shoehorn the uniform initialization fix into them.

This kind of discussion goes nowhere fast. My solution is designed to affect as little else as possible. It collides with nothing else, and it isn't related to any other issues. It's a tight, focused fix for a real deficiency in the language feature. You could argue that some of the constructs are superfluous, but as far as I'm concerned, as long as we get c: and l: into the spec, the problem is fixed.

Give the committee time, and they will squander it. And your suggestion gives them time. Which is exactly what you want: to make minimal progress now, then find the perfect solution later.

It is your quest for the perfect overall solution that is the enemy of progress towards any overall solution. I don't claim that my solution is perfect. But it is unquestionably a complete solution, resolving the ambiguity entirely.

This idea may sound political, but it is an effective way to force action. Prevent people from doing the minimum work needed to get by, and you force them to provide an actual working solution faster.

vattila...@yahoo.co.uk

unread,
Jan 27, 2013, 12:52:14 AM1/27/13
to std-pr...@isocpp.org, vattila...@yahoo.co.uk
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.
 
Regards,
Vidar Hasfjord
 

Nicol Bolas

unread,
Jan 27, 2013, 5:52:35 PM1/27/13
to std-pr...@isocpp.org, vattila...@yahoo.co.uk


On Saturday, January 26, 2013 9:52:14 PM UTC-8, vattila...@yahoo.co.uk wrote:
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.

That's part of my point: this is not an "extensive proposal". It breaks no existing code. The changes, in terms of volume of specification language, are minimal. It doesn't (to my knowledge) have any problems with the parser. It is short, simple, and focused, with no dependencies or other complications.

It's less disruptive and extensive than polymorphic lambdas to the language, and causes no breaking changes unlike the proposed changes to lambda capturing of `this`. Yet both of those are on deck for C++14. Why? Because we recognize that these are serious problems that must be corrected ASAP.

And some design space exploration and scrutiny may actually be good.

Considering the discussion here, which has tried to conflate solving this problem with a host of irrelevant personal issues (forwarding initializations, non-uniform initialization, your own suggestion to use non-existent named parameter syntax, etc), I don't see any of that as being "good". The last thing we want is for people to piggy-back their completely separate ideas onto this essential language feature.

This is no different from legislatures attaching "riders" to important bills which have nothing to do with the actual law in question, because they wouldn't be able to get them passed otherwise.

I don't see anything coming out of "exploring the design space" other than everyone trying to justify their own little pet features by fixing uniform initialization. I would much rather we fix uniform initialization by fixing uniform initialization and only that.

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.

I believe that eliminating those silent bugs should be done by improving uniform initialization. By C++14.

vattila...@yahoo.co.uk

unread,
Jan 27, 2013, 11:26:50 PM1/27/13
to std-pr...@isocpp.org
On Saturday, 26 January 2013 08:54:51 UTC, Nikolay Ivchenkov wrote:
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());
 
Sorry, you're right. I didn't read your post properly the first time around. 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 do
 
vector <int> a = {42}; // OK: Pass single element (copy-initialization excludes explict constructors).
 
or
 
vector <int> a (initializer_list <int> {42}); // OK: Pass single element.
 
Which is a mouthful, but at least it is very explicit, alerting the programmer to the resolution of ambiguity here.
 
Regards,
Vidar Hasfjord
 

Nikolay Ivchenkov

unread,
Jan 28, 2013, 7:19:30 AM1/28/13
to std-pr...@isocpp.org
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. This is one of the reasons why I don't like suggested {c:} and {l:}.

On Monday, January 28, 2013 8:26:50 AM UTC+4, vattila...@yahoo.co.uk wrote:
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 do
 
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.

3) There are constructs (such as new-expressions and mem-initializers) where only direct-initialization is available.

or
 
vector <int> a (initializer_list <int> {42}); // OK: Pass single element.
 
Which is a mouthful

while list-initializaiton is supposed to be terse :-)

Sebastian Gesemann

unread,
Jan 28, 2013, 9:30:54 AM1/28/13
to std-pr...@isocpp.org
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?

Cheers!
SG

Nicol Bolas

unread,
Jan 28, 2013, 7:58:50 PM1/28/13
to std-pr...@isocpp.org

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. That's one of the big reasons uniform initialization syntax exists. 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.
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...

Nicol Bolas

unread,
Jan 28, 2013, 8:04:06 PM1/28/13
to std-pr...@isocpp.org


On Monday, January 28, 2013 4:19:30 AM UTC-8, Nikolay Ivchenkov wrote:
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.

Except that it isn't; it's only used for calling constructors. It can't initialize aggregates. Also, "commonly" is something that we are trying to change with uniform initialization.
 

Malte Skarupke

unread,
Jan 28, 2013, 11:46:20 PM1/28/13
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. And that it's usually much easier to change code that uses fully specialized objects if it doesn't do what you expect it to do.
Imagine that you use a std::vector that uses an allocator that uses something similar to that construct function. (I bet several code bases have already started doing that to benefit from uniform initialization) And you use it all over your code to store aggregates. And then you use it with a std::deque<boost::any> for the first time. Which you can't copy using uniform initialization. There would be no good way to fix that, because your templated code is already used in too many places. My change would make sure that there wouldn't be a problem to begin with.

All other rules for example about ambiguity remain unchanged. The reason why construct_uniform<std::vector<int>>(10); is not ambiguous is that the constructor using initializer lists would require a user defined conversion.

Also note that partial specializations do also not prefer initializer list construction. So std::vector<T> ten{10}; Will always contain ten elements, no matter what T is. Only fully specialized templates and non-template types would prefer initializer list construction.

I think this doesn't break any existing code. If it does, the existing code probably didn't do what you expected it to do anyway.

The one problem that this would still have is this:
construct_uniform<std::vector<int>>(10); // create vector with ten elements
construct_uniform<std::vector<int>>(10, 20, 30); // create vector with three elements

I think that that is OK because it is easy to change this for the user (add {}) and is a better problem to have than the current situation. Because really at the moment you shouldn't use uniform initialization at all as soon as templates are involved. Because it may break on some type that you don't know about yet and then you won't be able to change it any more because that would break your code for other types.

So what do you guys think? Did I miss that this would break something obvious?

2013/1/28 Nicol Bolas <jmck...@gmail.com>
--
 
---
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.

Nicol Bolas

unread,
Jan 29, 2013, 12:37:36 AM1/29/13
to std-pr...@isocpp.org


On Monday, January 28, 2013 8:46:20 PM UTC-8, Malte Skarupke wrote:
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.

So instead of a short-and-simple language feature that's compact and easy to read, you want a big, bulky library feature that's connected to a language feature.

We can almost implement `construct_uniform` now, using `std::enable_if` (assuming that we get a `std::is_aggregate` traits class):

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)...);
}

The problem is that you can't use `construct_uniform` in any context. You must name the type T, so you're not able to get the equivalent of this:

SomeFunc(arg1, {c:...}, arg3);

If `SomeFunc`'s second parameter is type-deduced, then this code (using {}) would fail, because you can't use braced-init-lists with type deduction contexts. Or at least, not for a general `T`-style argument. And if it's not type deduced, then using {} syntax would give the inconsistent results above. Your resolution for this would be:

SomeFunc(arg1, construct_uniform(...), arg3);

Which gets in the way of the whole "not having to repeat the typename that the compiler can easily deduce" issue.

So no, this is not as good a solution; it only solves part of the problem.

Nikolay Ivchenkov

unread,
Jan 29, 2013, 5:34:15 AM1/29/13
to std-pr...@isocpp.org
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...}.

Malte Skarupke

unread,
Jan 29, 2013, 7:04:42 AM1/29/13
to std-pr...@isocpp.org

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.

--

Nicol Bolas

unread,
Jan 29, 2013, 7:55:09 AM1/29/13
to std-pr...@isocpp.org
On Tuesday, January 29, 2013 4:04:42 AM UTC-8, Malte Skarupke wrote:

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.

Here's one it doesn't solve:

void SomeFunc(std::vector<int> v);

SomeFunc({10});

There's no type deduction happening here.

Yes, it would mean that you have to fall back to () initialization sometimes,

If you have to abandon uniform initialization syntax, it's not solving the problem. It's hiding the problem. The proposal is called "Towards more uniform initialization", not less.
 

(as you do now) but it is superior to the current uniform initialization rules in that it doesn't give you unexpected behavior.

But it does give unexpected behavior. It gives the same unexpected behavior as it currently does in non-template deduction contexts. That's how you defined it. So something as simple as this:

std::vector<int> v{20};

Does not get fixed. You still cannot access std::vector<int>'s sizing constructor with uniform initialization syntax.

You're basically saying that it's safe(er) to use {} in templates, but not in non-template code.

You're too focused on just the template issue. The fundamental issue is the fact that the user has no ability to decide which constructors should be preferable. Coming up with ad-hoc rules as you did is no solution; we tried that with the current {} syntax, and it turns out that it didn't work. We have to accept that the user needs a way to explicitly state whether a particular braced-init-list should be used as an initializer list constructor parameter or as arguments to a constructor.

Also, it should be noted that this is probably the first feature that has been suggested where behavior of code changes based on the fact that a named type just so happened to have come from a template rather than being hard-coded. I don't like that idea; template code is not a special land where the rules of C++ apply differently. It should work the same as anywhere else.

Oh, and one more thing: it's a breaking change. Granted, it would only break code that's somewhat dangerous anyway, but it's still a breaking change. Mine isn't.

Nicol Bolas

unread,
Jan 29, 2013, 8:02:20 AM1/29/13
to std-pr...@isocpp.org


On Tuesday, January 29, 2013 2:34:15 AM UTC-8, Nikolay Ivchenkov wrote:
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.

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, but only in the case when the argument just so happens to be created from a braced-init-list?

Sebastian Gesemann

unread,
Jan 29, 2013, 8:11:18 AM1/29/13
to std-pr...@isocpp.org
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?

> 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.

Cheers!
SG

Nicol Bolas

unread,
Jan 29, 2013, 8:32:33 AM1/29/13
to std-pr...@isocpp.org


On Tuesday, January 29, 2013 5:11:18 AM UTC-8, Sebastian Gesemann wrote:
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?

Because if T is not an aggregate, there is a clear ambiguity about what exactly will be called. It could be a regular constructor; it could be an initializer_list constructor. I don't know, and I have no way of telling the system which I actually want to call.

Consider allocator_traits::construct. It cannot currently be used with aggregates (without creating your own specialization of the traits class for each aggregate); it will call this by default:

::new (static_cast<void*>(p)) T(std::forward<Args>(args)...)

However, it could be extended to support initializing aggregates:

::new (static_cast<void*>(p)) T{std::forward<Args>(args)...}

Only now, we're not calling constructors anymore if T isn't an aggregate. We might be interpreting what are supposed to be constructor parameters as members of an initializer list. That's bad.

::new (static_cast<void*>(p)) T{c: std::forward<Args>(args)...}

With the new syntax, we are guaranteed to be calling a constructor or performing aggregate initialization. That's good.

> 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.

OK, it does not work for the wrong reasons.

> 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.

It potentially does the wrong thing because the user has no way of explicitly declaring what the right thing is. As you said, you "guessed" that I meant that one might be invoked over another, but I explicitly left out what the "right" thing was to make my point. Without the ability for the user to say "I mean X", it is not clear what this code would do.

When the user wrote that braced-init-list, he had an idea in mind about how it would be used to initialize the object. He either meant to call an appropriate constructor, or he meant for it to be an initializer list. If he meant to call a constructor, he should get a compiler error. If he meant for it to be an initializer_list, he should get correct behavior.

Right now, we don't have a way to say either of these. We can only say "try both, but start with initializer_lists".

> 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.

No, but we do need to be explicit in saying, "never call a non-initializer_list constructor". Which is what the `l:` syntax says. Thus, if `Func` took a `std::vector<SomeType>` instead of a `std::vector<int>`, this would explicitly fail to compile by saying, "no available initalizer_list constructor" or something to that effect. Rather than the completely incorrect "attempt to call explicit constructor from copy-list-initialization", since the programmer's intent is not to call that constructor at all.

See the difference?

Nikolay Ivchenkov

unread,
Jan 29, 2013, 9:28:34 AM1/29/13
to std-pr...@isocpp.org
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).
 
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 that's the goal, it's not achieved.
 
If you want to argue against this goal, take it up with Stroustrup.

I've tried. He is happy to live with the explanation that the only reason why people don't like his beloved initialization syntax is that they just don't like all new/unfamiliar features. He's VIP who doesn't want to discuss technical details with plain people. This is my impression.
 
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

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 );

would be equivalent to

    AggregateType a = braced-init-list ;

while

    NonAggregateType na1 = ~ braced-init-list;

and

    NonAggregateType na2 ( ~ braced-init-list );

would consider only initializer-list constructors and the first parameter of the selected constructor would be initialized with the braced-init-list.


    struct A
    {
        std::string s;
        int n;
    };

    int main()
    {
        A a1 = ~{"text", 1};
        // equivalent to A a1 = {"text", 1};

        A a2(~{"text", 1});
        // equivalent to A a2 = {"text", 1};

        std::vector<T> v = ~{20, T()};
        // may create only two elements initialized with values 20 and T()

        std::vector<T> v(~{20});
        // may create only one element initialized with value 20
    }

Sebastian Gesemann

unread,
Jan 29, 2013, 9:41:29 AM1/29/13
to std-pr...@isocpp.org
The only c++11 problem w.r.t. initialization I am aware of so far is
that when writing generic code like a container's emplace function or
std::make_shared/unique you have to decide what kind of initialization
syntax you should be using. Each choice has pros and cons.

This approach will fail to initialize aggregates:

template<class T, class...Args>
unique_ptr<T> make_unique(Args&&...args) {
... new T(forward<Args>(args)...) ...
}

This approach will make it impossible to invoke certain constructors:

template<class T, class...Args>
unique_ptr<T> make_unique(Args&&...args) {
... new T{forward<Args>(args)...} ...
}

Think of T=vector<int> where you want this vector to have a certain
initial size, not contain some specific value.

This is unsatisfying.

On Tue, Jan 29, 2013 at 2:32 PM, Nicol Bolas wrote:
> On Tuesday, January 29, 2013 5:11:18 AM UTC-8, Sebastian Gesemann wrote:
I have trouble reading your mind on this one. What's the context? Do
you or don't you know what T? What kind of initialization (direct or
copy) are you thinking of?

> Consider allocator_traits::construct. It cannot currently be used with
> aggregates (without creating your own specialization of the traits class for
> each aggregate); it will call this by default:
>
> ::new (static_cast<void*>(p)) T(std::forward<Args>(args)...)

Right. This another example of a problem I acknowledge its existence
of. What I don't yet acknowledge is that this is a core language
problem. I also don't acknowledge that {c:...} or {l:...} fixes this
problem unless you want to offer two versions of construct. But
construct is not the only function like this. I mentioned earlier
make_shared, and the containers' emplace functions. Having to write
two versions of these functions is a major PITA.

I would much rather have a solution that does not require offering
multiple overloads of these functions. I'd much rather have overload
resolution rules take care of picking the right constructor. And I'd
much rather have perfect forwarding be able to pass such ambiguity
resolving things.

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)...});
}

int main() {
auto up = make_unique<vector<int>>(nolist,42);
// Deduction of Args: some unspecified special type, int
}

In my opinion, having different calling syntaxes () and {} modify
overload resolution is a bad idea. Overload resolution should just be
dependent on the parameter types so that one can write agnostic
forwarders that don't need to add special casing w.r.t. () and {}

> ::new (static_cast<void*>(p)) T{c: std::forward<Args>(args)...}
>
> With the new syntax, we are guaranteed to be calling a constructor or
> performing aggregate initialization. That's good.

And what about the case where you actually want the initialier_list
constructor? Do you plan to offer two construct functions? I hope not.

>> > 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.
>
> It potentially does the wrong thing because the user has no way of
> explicitly declaring what the right thing is. As you said, you "guessed"
> that I meant that one might be invoked over another, but I explicitly left
> out what the "right" thing was to make my point. Without the ability for the
> user to say "I mean X", it is not clear what this code would do.
>
> When the user wrote that braced-init-list, he had an idea in mind about how
> it would be used to initialize the object. He either meant to call an
> appropriate constructor, or he meant for it to be an initializer list. If he
> meant to call a constructor, he should get a compiler error. If he meant for
> it to be an initializer_list, he should get correct behavior.

It's hard to comment on this. It was an artifical example with an
unknown signature of Func. If Func took a vector<int> I would say the
code looks fine.

The general rule should be: Don't overload too eagerly. It has been
mentioned before and I would like to repeat it again: It can be argued
that the overloading of vector's constructor is unfortunate. What I
don't like about your proposal is that you invent a new way of
controlling overload resolution. IMHO, overload resolution should just
depend on the parameters which should all be forwardable.

> Right now, we don't have a way to say either of these. We can only say "try
> both, but start with initializer_lists".
>
>> > 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.
>
> No, but we do need to be explicit in saying, "never call a
> non-initializer_list constructor". Which is what the `l:` syntax says. Thus,
> if `Func` took a `std::vector<SomeType>` instead of a `std::vector<int>`,
> this would explicitly fail to compile by saying, "no available
> initalizer_list constructor" or something to that effect. Rather than the
> completely incorrect "attempt to call explicit constructor from
> copy-list-initialization", since the programmer's intent is not to call that
> constructor at all.
>
> See the difference?

Right. Then you probably my idea from above to include a counterpart
of "nolist". ;-)

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.

Cheers!
SG

Nikolay Ivchenkov

unread,
Jan 29, 2013, 10:13:49 AM1/29/13
to std-pr...@isocpp.org
On Tuesday, January 29, 2013 6:41:29 PM UTC+4, Sebastian Gesemann wrote:

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)...});
  }

What would you suggest to do with existing templates, such as emplace and make_shared?

vattila...@yahoo.co.uk

unread,
Jan 29, 2013, 1:04:51 PM1/29/13
to std-pr...@isocpp.org
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".
 
Regards,
Vidar Hasfjord
 

vattila...@yahoo.co.uk

unread,
Jan 29, 2013, 2:07:54 PM1/29/13
to std-pr...@isocpp.org
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:
 
 
2) In the following case

    vector <int> a = {42, 1};

there are two viable converting constructors. 
 
True, since this does not involve explicit constructors, you cannot use copy-initialization to disambiguate. 
 
Regards,
Vidar Hasfjord
 

Nikolay Ivchenkov

unread,
Jan 29, 2013, 2:40:03 PM1/29/13
to std-pr...@isocpp.org
On Tuesday, January 29, 2013 10:04:51 PM UTC+4, vattila...@yahoo.co.uk wrote:

> 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.

Such overload set might work perfectly well with a different model of initialization. Currently we can't simply use

    auto f = &std::vector<int>::push_back

at least because std::vector<int>::push_back is overloaded, so we should consider this as an example of bad library design too, huh? I'd rather prefer to think that & is too weak and its use should be limited.

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: "restrict use of broken things like list-initialization" :-)


On Tuesday, January 29, 2013 11:07:54 PM UTC+4, vattila...@yahoo.co.uk wrote:
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.
See also http://liveworkspace.org/code/21QHT3$0
 

vattila...@yahoo.co.uk

unread,
Jan 29, 2013, 4:24:09 PM1/29/13
to std-pr...@isocpp.org
On Tuesday, 29 January 2013 19:40:03 UTC, Nikolay Ivchenkov wrote:
I'm sure, and the diagnostic message clearly says that the explicit constructor is selected.
 
Thanks, I finally get your point. To recap and correct myself:
 
vector <int> v = {42};
 
If the il-preference rule was revoked, this would not work to disambiguate in favour of the il-constructor. The fact that the size-constructor is explicit doesn't exclude it from the overload set. That would need a special rule that disambiguated in favour of the il-constructor in specific contexts, which would just complicate things.
 
Which leaves us with the mouthful option:
 
vector <int> a (initializer_list <int> {42}); // OK: Pass single element.
 
Regards,
Vidar Hasfjord
 

Nicol Bolas

unread,
Jan 29, 2013, 10:05:50 PM1/29/13
to std-pr...@isocpp.org


On Tuesday, January 29, 2013 6:28:34 AM UTC-8, Nikolay Ivchenkov wrote:
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).

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.

That's hardly "two different kinds of initialization."

As stated in the proposal, the ideal solution would have been to make {} never translate into initializer list constructors to begin with. So if you wanted to make a vector<int> of one element, you use {{42}}. But since that ship has sailed, this is the best solution left: allow the user to pick one or the other on a case-by-case basis.

We do the best with what we have.


That's what {l:} does.

Furthermore, {l:} is not enough, because there's no way to have similar behavior but only consider regular constructors. That's what {c:} is for.

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. I understand that, but since this proposal is all about making that possible your idea isn't helpful.

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.

Nicol Bolas

unread,
Jan 29, 2013, 10:17:55 PM1/29/13
to std-pr...@isocpp.org


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.

It's interesting that the proposed language is the exact opposite of how {} works now. It picks constructors first, then initializer_list constructors if no matching constructor is found.

Of course, this just transplants one problem with another: with this, the regular constructors are shadowing possible initializer_list constructors. However, at least this way, the user does have the ability to use an initializer_list constructor by passing an initializer_list when they mean to use one. It may be bulkier, but it does work.

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)

> 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?

Nicol Bolas

unread,
Jan 29, 2013, 10:25:53 PM1/29/13
to std-pr...@isocpp.org


On Tuesday, January 29, 2013 7:17:55 PM UTC-8, Nicol Bolas wrote:

> 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?

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.

Nevin Liber

unread,
Jan 29, 2013, 10:34:12 PM1/29/13
to std-pr...@isocpp.org
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...
 
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?

Without destroying the language as a whole in the process.
 

--
 
---
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.
 
 



--
 Nevin ":-)" Liber  <mailto:ne...@eviloverlord.com(847) 691-1404

Nicol Bolas

unread,
Jan 29, 2013, 11:10:46 PM1/29/13
to std-pr...@isocpp.org


On Tuesday, January 29, 2013 7:34:12 PM UTC-8, Nevin ":-)" Liber wrote:
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.

That doesn't mean that they're all wrong.

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...

static if. It's already in development, with successful implementations in another language in a similar capacity.

Sebastian Gesemann

unread,
Jan 30, 2013, 8:36:21 AM1/30/13
to std-pr...@isocpp.org
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?


On Wed, Jan 30, 2013 at 4:34 AM, Nevin Liber wrote:
> On 29 January 2013 21:17, Nicol Bolas wrote:
>
>> 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...

Concepts really tried to kill three birds with one stone:
- constraining templates
- modular type checking (checking templates before instantiation)
- concept-based overloading

enable_if ist just about the first one and probably much easier to
implement as a core language feature.

Cheers!
SG

Nikolay Ivchenkov

unread,
Jan 30, 2013, 8:50:25 AM1/30/13
to std-pr...@isocpp.org
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.

Furthermore, {l:} is not enough, because there's no way to have similar behavior but only consider regular constructors.

We have () for 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.

() could be used everywhere if we would have a regular declaration syntax, e.g.:

    void f(/std::vector<int> v(n)); // default value of v is std::vector<int>(n)
 
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};
 
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 Wednesday, January 30, 2013 8:10:46 AM UTC+4, Nicol Bolas wrote:
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.

I wouldn't consider static if as a full-fledged replacement for enable_if.
 
It's already in development, with successful implementations in another language in a similar capacity.

AFAIK, "another language" does not have two-phase name lookup.

Nicol Bolas

unread,
Jan 30, 2013, 3:50:13 PM1/30/13
to std-pr...@isocpp.org
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.

The difference is that it is syntactically within the "expression" rather than outside of it.

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, that aggregate initialization should be considered a form of list initialization, and that if you want to initialize an object that may or may not be an aggregate, you should use std::enable_if or other compile-time testing mechanisms to do so.


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. I was saying that this proposal only affects the overload resolution rules of uniform initialization.

Second, I wanted to pin down the feature and make sure that there are no issues with it before getting into details of wording. Also, while I know the general form it would take and what would need to be changed (just 8.5.4 to define the grammar, and 13.3.1.7 to express the meaning of that grammar), I'm less certain of my understanding of what the right way to make that change is.

Specifically, 13.3.1.7 does not make mention of braced-init-list's at all; it makes mention of list-initialization vs. copy-list-initialization. I don't know if the language simply worked out that way or if it was deliberate, so I don't feel comfortable modifying this section to mention some new `qualified-braced-init-list` subcategory. But the only way around that is to have 8.5.4 define several sub-types of list-initialization for the different constructor methodologies.

But this is all merely a wording implementation detail; it has no material effect on the behavior. I didn't want to commit to a wording until I had a syntax I was happy with.

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.

The proposal says:

l”: Call an initializer_list constructor.

And it also says:

These variation only affect which constructors are considered during overload resolution; in every other respect, they work like a regular braced-init-list.

I'm not sure how I could make it more clear that this syntax only affects constructor selection, that every other aspect of braced-init-list initialization is unaffected. Obviously adding spec wording would make it clearer, but I suppose I could reorganize the Design Overview section.

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. My way is far more human readable.

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>.

This is a problem for which we presently have the means to resolve: by using an explicit typename. The general solution for explicitly resolving ambiguity is using explicit syntax to make clear what was implicit before.

All I'm proposing is an appropriate ambiguity resolution syntax for uniform initialization. Allow the user to specify if they want to call non-initializer_list constructors or initializer_list constructors, or which to prefer?

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.

Did we? In what case would {c:} usage for all of the currently constructor-based initialization break code?

Indeed, as vattila pointed out, we already have a defect report suggesting that we allow allocator_traits<>::construct to detect aggregates and different constructor types, and apply uniform initialization in certain cases. The proposed mechanism is the exact equivalent of the proposal's {cl:} behavior.

So I would say that {c:} does not break any code that currently uses () behavior. Outside of forbidding narrowing and other elements of uniform initialization and such.

Nicol Bolas

unread,
Jan 30, 2013, 4:23:01 PM1/30/13
to std-pr...@isocpp.org


On Wednesday, January 30, 2013 5:36:21 AM UTC-8, Sebastian Gesemann wrote:
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.

That's an implementation difference only; conceptually they are the same: there is an ambiguity, so you add a syntax construct to resolve it. Yes, one changes the argument's expression, but braced-init-lists aren't expressions. They will therefore have to use different syntax from expressions to resolve the ambiguity.

Would it make you feel better if it were some kind of prefix syntax of the {} rather than a syntax within the {} enclosure? Because the form of the syntax is personal preference; what I'm concerned with is the behavior. I choose the syntax I did because it doesn't cause any parsing problems with existing code, thus allowing me to use special identifiers rather than new keywords or oddball symbols.

A prefix syntax would be much harder to include the 4 use cases I outlined.
 
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.

The fact is, once uniform initialization shipped with the ambiguity as part of it, with the ability for {} to resolve to an initializer_list constructor at all, the damage was done. And since we can't remove that feature, our only choice is to find a way to let the user specify how they want the ambiguity resolved.

That's all I'm proposing here. It's not a different initialization syntax; it's a slight modification of the same initialization syntax. In every other respect, it works identically to {}.

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:}.

This argument is off-topic. You're talking about the problem of forwarding initialization intent. That's a problem that exists now; there's no way to make the called function use a set of parameters with {} syntax instead of () syntax. My proposal does not aim to fix this situation.

In short, forwarding initialization intent is a problem now. It would be a problem even with this proposal. But this proposal is not trying to solve that problem. So the fact that it's a problem is irrelevant to this discussion. Yes, the proposal makes the problem slightly worse in that there are more forms of initialization to forward. But we already have two forms currently: braced-init-list vs. () syntax.

Adding variations of braced-init-lists to any such resolution will be no more difficult than resolving the problem in the first place.

Personally, I don't think the problem can be resolved. At some level, we are going to have to accept that the forwarding problem can never be fully resolved. Variadic templates and rvalue references solved some of the most pernicious issues, but even they aren't perfect.

Forwarding braced-init-lists as braced-init-lists is impossible, since they cannot be values. You can't store them, by definition. So they can't go into a parameter pack. My syntax doesn't change this fact, nor should it. That's not the point of this proposal.

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.

The problem with this solution, as covered by the proposal, is that it only fixes the problem for one type: the type that implements the disambiguation parameter. Which means it has to be done by everyone, everywhere.

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.

The language solution is consistent and uniform for all users. It interoperates, and is much more readable overall. It doesn't require library authors to do anything at all.
 
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?

Language changes are anything but quick fixes.

DeadMG

unread,
Jan 30, 2013, 4:47:25 PM1/30/13
to std-pr...@isocpp.org
IYAM, the simplest way to deal with aggregates is to simply cut aggregate initialization, and then say that aggregates have implicitly generated explicit constructors. So that for

struct agg {
    int i;
};

You could consider it as

struct agg {
    agg() = default;
    agg(const agg&) = default;
    agg(agg&&) = default;
    explicit agg(int __i = int())
        : i(std::move(__i)) {}
    int i;
};

This, AFAIK, should allow all existing code to continue to compile and behave the same without changes under uniform initialization. It would also permit more compatibility with C++03 code that uses () initialization.

 At some level, we are going to have to accept that the forwarding problem can never be fully resolved.

It can't be without some new syntax, but it certainly could be in general without any breaking changes. It really depends on how much you'd be willing to "pay" in terms of new features/idioms/etc. 

vattila...@yahoo.co.uk

unread,
Jan 31, 2013, 2:12:02 AM1/31/13
to std-pr...@isocpp.org
On Wednesday, 30 January 2013 21:47:25 UTC, DeadMG wrote:
IYAM, the simplest way to deal with aggregates is to simply cut aggregate initialization, and then say that aggregates have implicitly generated explicit constructors.
 
Agree. I suggested this way to deal with aggregates as well earlier in this thread. Moving towards treating all initialization as construction gets us closer to one day, maybe, introducing language support for tuples (which has been my preference since the "uniform initialization" feature was in its infancy, and I first encountered the awkward initializer_list type with horror). The challenge then is to define all current initialization behaviour in terms of conversions of the built-in tuple type.
 
At that point, disambiguation can be done through the type system, as is the ingrained way of doing things in C++.
 
Regards,
Vidar Hasfjord
 

Nikolay Ivchenkov

unread,
Jan 31, 2013, 5:38:50 AM1/31/13
to std-pr...@isocpp.org
On Thursday, January 31, 2013 12:50:13 AM UTC+4, Nicol Bolas wrote:
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.

float and int aren't essential parts of the conversion syntax. They are operands, unlike your c and l.
 
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.

c and l are contived arguments, they don't give any capabilities. Pair {c:} + {l:} can be replaced with (:) + {:} so that the functionality will be the same. Keywords static_cast and const_cast could also be considered as contrived operands of explicit type conversion, but there is no point in such interpretation.

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,

I'm not even sure that "uniform" has the same meaning for us.
 
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.

"Localized" is an abstract advantage. I saw and can imagine a lot of very simple modifications to normative wordings, that affect several places in the standard.
 
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.

That answer is unrelated to my questions about similarity.

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.

That's not so.


    template<typename T>
    void Func()
    {
        T arr = ~{10, 20};

        //Do stuff with arr
    }

    void Func<int[2]>();
    void Func<std::array<int, 2>>();
    void Func<std::vector<int>>();

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?

Well, now I see why you don't understand objections. This is just because you are not interested in thorough consideration of objections. Thus, there are no reasons to continue the conversation with you.

Nikolay Ivchenkov

unread,
Jan 31, 2013, 5:43:09 AM1/31/13
to std-pr...@isocpp.org
On Thursday, January 31, 2013 1:47:25 AM UTC+4, DeadMG wrote:
IYAM, the simplest way to deal with aggregates is to simply cut aggregate initialization, and then say that aggregates have implicitly generated explicit constructors.

How would you suggest to distinguish lvalues and rvalues?

    struct X
    {
        std::string s1, s2;
        std::unique_ptr<std::string> p;
    };

    int main()
    {
        std::string s = "string";
        X x = { "text", s, std::unique_ptr<std::string>(new std::string(s)) };
    }

What signature should the aggregate ctor of X have?

DeadMG

unread,
Jan 31, 2013, 6:44:34 AM1/31/13
to std-pr...@isocpp.org
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 be

    X(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 as

    X(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.


Sebastian Gesemann

unread,
Jan 31, 2013, 7:26:58 AM1/31/13
to std-pr...@isocpp.org
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:
>> 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.
>
> That's an implementation difference only; conceptually they are the same:
> there is an ambiguity, so you add a syntax construct to resolve it. Yes, one
> changes the argument's expression, but braced-init-lists aren't expressions.

They are not expressions but its elements are expressions. However, c:
or l: are not expressions but part of your initialization syntax.

> They will therefore have to use different syntax from expressions to resolve
> the ambiguity.

I already provided a counter example to that. Here is it again:

vector<int> foo {nolist,23,42};

Here "nolist" is actually the list's first element of a special type
that would be discarded during overload resolution for constructors.
This is a truly uniform syntax: just curly braces. You can use this
syntax for every initialization and control overload resolution by
providing the appropriate arguments. Having this truly unique syntax
allows us to use plan {} in allocator<>::construct and so on.

> Would it make you feel better if it were some kind of prefix syntax of the
> {} rather than a syntax within the {} enclosure?

No. It does not make a difference. I still would not consider this an
improvement w.r.t. uniform initialization because there really isn't
anything uniform about it, if you still have to use at least two
syntaxes to be able to get any kind of inizialitazion you want.

>> 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.
>
> The fact is, once uniform initialization shipped with the ambiguity as part
> of it, with the ability for {} to resolve to an initializer_list constructor
> at all, the damage was done.

Yes. I agree.

> And since we can't remove that feature, our
> only choice is to find a way to let the user specify how they want the
> ambiguity resolved.

They can do this already by using () and {}. It may not be as explicit
or restrictive as you want, but we already have two syntaxes to get
every kind of initialization.

> That's all I'm proposing here. It's not a different initialization syntax;

On that we disagree. And that is why I don't consider your proposal to
be an improvement towards uniform initialization.

>> 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:}.
>
> This argument is off-topic. You're talking about the problem of forwarding
> initialization intent. That's a problem that exists now; there's no way to
> make the called function use a set of parameters with {} syntax instead of
> () syntax. My proposal does not aim to fix this situation.

You should not strive for improving C++ by adding a feature that just
fixes initialization in _some_ contexts. You should strive for
proposing something that fixes initialization in every context.
Proposing a feature that fixes only half of the problems with respect
to initialization i a kind of quick fix that you may regret later when
it gets accepted, don't you think?

> In short, forwarding initialization intent is a problem now. It would be a
> problem even with this proposal. But this proposal is not trying to solve
> that problem. So the fact that it's a problem is irrelevant to this
> discussion.

I disagree. It doesn't have to be.

> The problem with this solution, as covered by the proposal, is that it only
> fixes the problem for one type: the type that implements the disambiguation
> parameter. Which means it has to be done by everyone, everywhere.

This is not true.

> 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.

> Language changes are anything but quick fixes.

I hope they are not.


Cheers!
SG

Mikhail Semenov

unread,
Jan 31, 2013, 8:54:19 AM1/31/13
to std-pr...@isocpp.org
I am sorry, but doesn't it look ugly:
foo x{nolist, 29};
instead of the usual
foo x(29);
 
My point is, should it be as follows:
foo x(29); // call foo(int);
foo x{29}; // call foo(initializer_list<int>)
 
If {} are used the preference should be for the initializer list, and if () the "standard" constructor should be preferred.
If, on the other hand, a constructor with an initializer list is absent, either option is possible.
In this case, things will be clear, and we will avoid an "ugly", new syntax invention.
 
Mikhail.


Cheers!
SG

Arthur Tchaikovsky

unread,
Jan 31, 2013, 8:56:31 AM1/31/13
to std-pr...@isocpp.org
Couldn't agree more with Mikhail

Nikolay Ivchenkov

unread,
Jan 31, 2013, 9:19:03 AM1/31/13
to std-pr...@isocpp.org

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.

Finally, AggregateType(x) and AggregateType{x} may have different meaning - see example below:

    #include <cstddef>
    #include <iostream>
    #include <string>
    #include <utility>

    #define FORWARD(x) static_cast<decltype(x) &&>(x)

    template <class T1, class T2>
        struct P
    {
        template <class U1, class U2>
            P(U1 &&u1, U2 &&u2) :
                p(FORWARD(u1), FORWARD(u2)) {}

        template <class T>
            operator T() const
                { return {p.first, p.second}; }

        std::pair<T1, T2> p;
    };


    struct A
    {
        std::string s;
        std::size_t n;
    };

    int main()
    {
        P<std::string, std::size_t> p("whole", 1u);
        A a1(p);
        A a2{p};
        std::cout << a1.s << " " << a1.n << std::endl;
        std::cout << a2.s << " " << a2.n << std::endl;
    }

http://liveworkspace.org/code/1MU15f$0

Sebastian Gesemann

unread,
Jan 31, 2013, 9:41:20 AM1/31/13
to std-pr...@isocpp.org
On Thu, Jan 31, 2013 at 2:54 PM, Mikhail Semenov wrote:
> I am sorry, but doesn't it look ugly:
> foo x{nolist, 29};
> instead of the usual
> foo x(29);

Nobody would force you to write

foo x {nolist,29};

instead of

foo x (29);

You could still write the latter if you want.

The point of "nolist" is to make the {}-syntax usable for any kind of
initialization you would want. And being explicit about not wanting to
use a possibly existing initializer_list constructor is not such a bad
thing.

How would you implement an allocator's construct function template?

new(ptr) T(forward<Args>(args)...); // #1
new(ptr) T{forward<Args>(args)...}; // #2

With #1 you can't initialize aggregates and picking the
initializer_list constructor of a vector<int> requires you to
explicitly pass a std::initializer_list object. With #2 and
T=vector<int> you can't initialize the vector to a certain size.

I argue that this problem exits because of the syntax-dependent
overload resolution rules for initialization.

I don't know what "uniform initialization syntax" means to you, but to
me it means that there is one syntax that is applicable in every
situation to do whatever you like.

Things are not perfect but I'm actually not proposing any new feature
here. It was just a counter example to one of Nicol's statements.
Personally, I don't like adding new special rules as a work around for
another already existing special rule like {} preferring initializer
list constructors.

I guess the only suggesten I'm making here is that people think hard
before creating many constructor overloads for their own classes and
learn to live with imperfections like not being able to easily invoke
a std::initializer_list constructor using emplace or make_shared.

I'd like to mention that IIRC a commitee member hinted at removing the
irregularity between auto and function template argument deduction.
You all probably know that

auto x = {1,2,3,5,8};

makes x an initializer_list<int> and that this currently only works
for auto. If it could be made to work for templates as well, we can at
least get

make_shared<vector<int>>({1,2,3,5,8})

to work. It won't work with nested lists, though, as far as I can tell.


Cheers!
SG

Mikhail Semenov

unread,
Jan 31, 2013, 9:53:59 AM1/31/13
to std-pr...@isocpp.org
There still cane be a solution: allow conversion to a vector or any collection. But if there is an explicit contructor with the same number of parameter, it should be selected when () are used; and when you use {} the vector (or whatever collection) should be converted to initializer list (if initializer list is present).



Cheers!
SG

Nicol Bolas

unread,
Jan 31, 2013, 11:51:25 AM1/31/13
to std-pr...@isocpp.org


On Thursday, January 31, 2013 4:26:58 AM UTC-8, Sebastian Gesemann wrote:
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.

OK, now we're getting somewhere; I didn't get that you were talking about a language change that keys off of a specific library type.

I actually rather like this. I'm not certain it needs to be that verbose, since I'd like to see it used more globally. I'd hate to have to do `vector.emplace_back(std::nolist, ...)` all the time. And I'd also like a companion `inlist` type that forces the rest of a braced-init-list to be considered an initialization list. That way, you can express your intent to not call a constructor.

Personally however, I don't see any difference between these two approaches in terms of which initialization is more "uniform". Both require specific language changes to 8.5.4 and 13.3.1.7 in order to make them work. Both require the user to use special syntax. And so froth. The fact that it's a parameter type vs a syntactic construct is just an implementation detail.

Yes, this "implementation detail" means that one is more easily forwarded than the other. But I don't feel that one being directly forwarded and one not makes it more or less "uniform". You're still changing how the language interprets the braced-init-list, which is what you argued was non-"uniform" about my solution.

But ultimately, what I care about is that the ambiguity is fixed, while still using uniform initialization syntax. And this does that. So I'm content with this syntax, in general.

vattila...@yahoo.co.uk

unread,
Jan 31, 2013, 5:22:23 PM1/31/13
to std-pr...@isocpp.org

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

Nicol Bolas

unread,
Jan 31, 2013, 5:44:27 PM1/31/13
to std-pr...@isocpp.org

What purpose does this serve besides complicating the feature? We have no need for this "cv qualifier" in any other place than with braced-init-lists and parameters to forwarding functions leading to a braced-init-list.

It also makes it look uglier, since `nolist` appears to be modifying the value. Does that mean that each value in the braced-init-list should have `nolist` applied to it? If I have `std::vector<int>{30, 40}`, am I supposed to do `std::vector<int>{nolist(30, 40)}` or `std::vector<int>{nolist(30), 40}` or `std::vector<int>{nolist(30), nolist(40)}`? Which element does the compiler look to to decide whether to look at initializer-list constructors?

How does it work with copying elements and so forth; does it return an rvalue-reference, thus potentially provoking an unnecessary (depending on how it is defined)?

Having it be a parameter with a type makes it work effectively with other things (ie: forwarding), and it makes the syntax much clearer to the user in how it is meant to be applied. It doesn't modify a value; it is a value. It also allows you to add new types, like `inlist` which forces the use of initializer_list constructors and fails if none are found.

I don't see the point of "exploring the design space" if that exploration doesn't actually uncover something that's better in any way. It's just different.

DeadMG

unread,
Feb 1, 2013, 5:24:08 AM2/1/13
to std-pr...@isocpp.org
On Thursday, January 31, 2013 2:19:03 PM UTC, Nikolay Ivchenkov wrote:
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 be

    X(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 as

    X(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.

The constructor is a magic compiler function. It can be magically efficient. In addition, defining () in terms of {} would make for a simpler change, but wouldn't reduce the Standard complexity, I think, whereas removing aggregate initialization in general would.
Well, that's a nasty surprise. I hadn't considered a data type that could convert to both an aggregate structure as a whole, and a single member within it. Under the proposed change, this would be ambiguous.
It is loading more messages.
0 new messages