Whatever happened to LWG 2089?

170 views
Skip to first unread message

Nicol Bolas

unread,
Dec 31, 2016, 11:47:30 AM12/31/16
to ISO C++ Standard - Future Proposals
LWG 2089 is a library issue about `emplace`-style functions not being able to perform aggregate initialization. While it is expressed specifically for `allocator::construct`, new C++ changes (the deprecation of `allocator::construct`, `make_shared/unique`, the `in_place_t` constructors for `any` et. al) would suggest that any appropriate fix be applied elsewhere too.

The issue itself is still active. It has the status "EWG", due to wanting to investigate whether Evolution would like to pursue a language-based solution to such initialization issues (ie: asking if EWG is OK with the library essentially creating a new form of initialization).

This was presented in N4462 at Lexana, apparently. The EWG issue, #185, is set to the status "Ready", with the comment that EWG is fine with this being a library-based solution. That was in mid-2015.

So... now what? The LWG issue seems to be waiting for EWG, which made their decision back in 2015. And yet, nothing has gone anywhere. Is the issue moving forward?

I bring this up in part because I think LWG 2089 is flawed. Or more to the point, I would rather it be restricted specifically to aggregate initialization.

Basically, the current solution presents the exact opposite of the classic list-initialization problem: when constructors and `initializer_list` constructors conflict. Consider a `vector<vector<int>>`. If you call `emplace_back(5, 3, 2)`, you get an `initializer_list` constructor. If you call `emplace_back(5, 3)`, you get a vector of 5 integers with the value 3. As mentioned in 2089, you can work around that by explicitly asking for an `initializer_list`: `emplace_back(initializer_list<int>{5, 3})`.

And since this works exactly the opposite of list-initialization, I think people would not expect it. Or worse, they'll be bitten by this bug once they hear that they can use `emplace` to provoke list-initialization.

I think it would be best that, if you want an `initializer_list` constructor, you always have to spell it out. It makes it much more clear to the reader what's going on. Further, if a user adds a new constructor to the type that happens to match an `initializer_list` sequence, that won't suddenly break someone's `emplace` code.

Note: I am aware of how painfully verbose `std::initializer_list<type>` is. But frankly, that is a general problem in C++, one I believe we can solve.

Essentially, I say that LWG 2089 should be exclusively about adding aggregate initialization support to indirect forms of initialization (my term for all kinds of `emplace`-like behavior). It should be explicitly restricted from invoking list-initialization for non-aggregates. Of course, doing that in the library would require an `is_aggregate<T>` trait. But quite frankly, we've needed that trait for some time now.

Basically, the fundamental conceit of list-initialization is the belief that all you need in order to initialize an object is a list of values. After years of using list-initialization, I think we've proven that this is not true: that there really are different forms of initialization beyond just the parameters, and that these forms have to be explicitly provided. And we should not encode this false believe into indirect initialization APIs.

Ville Voutilainen

unread,
Dec 31, 2016, 12:06:05 PM12/31/16
to ISO C++ Standard - Future Proposals
On 31 December 2016 at 18:47, Nicol Bolas <jmck...@gmail.com> wrote:
> LWG 2089 is a library issue about `emplace`-style functions not being able
> to perform aggregate initialization. While it is expressed specifically for
> `allocator::construct`, new C++ changes (the deprecation of
> `allocator::construct`, `make_shared/unique`, the `in_place_t` constructors
> for `any` et. al) would suggest that any appropriate fix be applied
> elsewhere too.
>
> The issue itself is still active. It has the status "EWG", due to wanting to
> investigate whether Evolution would like to pursue a language-based solution
> to such initialization issues (ie: asking if EWG is OK with the library
> essentially creating a new form of initialization).
>
> This was presented in N4462 at Lexana, apparently. The EWG issue, #185, is
> set to the status "Ready", with the comment that EWG is fine with this being
> a library-based solution. That was in mid-2015.
>
> So... now what? The LWG issue seems to be waiting for EWG, which made their
> decision back in 2015. And yet, nothing has gone anywhere. Is the issue
> moving forward?

Well, first of all, "The notes in Lenexa say that Marshall & Jonathan
volunteered to write a paper on this".
Marshall and Jonathan have been far too busy with other things, so I'm
not expecting anything
in this area for C++17. Beyond that, the problem needs further work.

> I bring this up in part because I think LWG 2089 is flawed. Or more to the
> point, I would rather it be restricted specifically to aggregate
> initialization.

The general problem is that it's impossible for a wrapper to convey
initialization really-perfectly.
Currently, a wrapper like make_unique has to make a decision - whether
to paren-init or whether
to brace-init, because that still can make a difference. N4462
explores potential solutions, but
doesn't nail down any particular one.

> I think it would be best that, if you want an `initializer_list`
> constructor, you always have to spell it out. It makes it much more clear to

But that's not the problem. If you have an initializer_list
constructor, you can give an initializer_list
to a wrapper and it *can* forward it(*). But what the wrapper can't do
is know whether your
target type to be wrapped can or cannot be paren-initialized, or
whether that target type
*requires* brace-init. Like aggregates do.
(*) Well, ok, unicorn initialization and imperfect forwarding can't
know that your potential
braced-list is meant to be an initializer_list.

> Essentially, I say that LWG 2089 should be exclusively about adding
> aggregate initialization support to indirect forms of initialization (my
> term for all kinds of `emplace`-like behavior). It should be explicitly
> restricted from invoking list-initialization for non-aggregates. Of course,
> doing that in the library would require an `is_aggregate<T>` trait. But
> quite frankly, we've needed that trait for some time now.

We have? Where? Evidence, please? Note: I'm not outright claiming that
we couldn't
*use* such a trait in some cases. I'm asking what evidence supports such a trait
and makes it the *correct* solution to the problem space of LWG 2089 and N4462.

In order to make substantiated claims about any of it, I
(want/need/)demand to see
- an implementation of the solution, whatever it is
- some findings on what its impact on an existing testsuite is
- preferably some findings on what its impact on non-testsuite code is.

Prior to that, all talk is cheap. The reason why I, Marshall nor
Jonathan haven't tried to slam-dunk
anything towards LEWG yet is that we have none of the above.
Regardless of "who the f*** are you",
Marshall and Jonathan are stdlib maintainers, and wrt "who the f*** am
I", I wouldn't let any solution
in this problem space anywhere near C++ without doing the homework
mentioned above.
The reason being that the initialization rules, and library facilities
above, wrapping and accommodating
them were in a state of flux to the very end of standardizing C++11.
To change something in this
area *requires* care. We have existing users relying on the current
rules, sane or not (both the users
and the rules), and changing them requires rationale, motivation,
proof-of-concept and testing.

I am, Yours Most Sincerely,
Ville Voutilainen,
The Chair of The Evolution Working Group

Ville Voutilainen

unread,
Dec 31, 2016, 1:07:02 PM12/31/16
to ISO C++ Standard - Future Proposals
On 31 December 2016 at 19:06, Ville Voutilainen
<ville.vo...@gmail.com> wrote:
>> Essentially, I say that LWG 2089 should be exclusively about adding
>> aggregate initialization support to indirect forms of initialization (my
>> term for all kinds of `emplace`-like behavior). It should be explicitly
>> restricted from invoking list-initialization for non-aggregates. Of course,
>> doing that in the library would require an `is_aggregate<T>` trait. But
>> quite frankly, we've needed that trait for some time now.
>
> We have? Where? Evidence, please? Note: I'm not outright claiming that
> we couldn't
> *use* such a trait in some cases. I'm asking what evidence supports such a trait
> and makes it the *correct* solution to the problem space of LWG 2089 and N4462.


Let me elaborate: which is the right question to ask:
- whether a type is an aggregate?
- whether a type can be initialized in a certain fashion?

In general, it seems better to be able to query whether a certain
expression is valid
for an object of a type, rather than what properties a (class) type
has. What motivates an is_aggregate
trait, rather than being able to query whether
aggregate(-like)-initialization is valid
for that type?

Nicol Bolas

unread,
Dec 31, 2016, 5:36:19 PM12/31/16
to ISO C++ Standard - Future Proposals
"Impossible" is a very strong word ;)

Now to be fair, it is completely impossible to forward every form of initialization perfectly... so long as you restrict the problem domain such that parameters are only intended to be used to initialize the object.

But if we expand things a bit, if we allow the type of a parameter to statically control the form of initialization:

template<typename T, typename ...Args>
T initialize
(Args ...args)
{
   
return T(std::forward<Args>(args)...);
}

template<typename T, typename ...Args>
T initialize
(std::construct_init_t, Args ...args)
{
   
return T(std::forward<Args>(args)...);
}

template<typename T, typename ...Args>
T initialize
(std::list_init_t, Args ...args)
{
   
return T{std::forward<Args>(args)...};
}

By using these control parameters, it is entirely possible to directly control exactly how to initialize an object. It's not "perfect" (due to narrowing and other issues), but it's as close as C++ can get to you actually writing the initialization yourself.

Now, I'm not saying I want to see this standardized. I'd much rather use the ad-hoc approach outlined in LWG 2089. But it should be something to think about.

And maybe... we could do both. If the primary overload of `initialize` there used LWG 2089's rules (or my expanded rule, below), but the typed overloads did not, that would allow people to have a very usable default, but still be able to choose what they wanted.

Well, asking `is_aggregate` alone is a non-starter:

struct Agg
{
   
int i;
};

class Value
{
public:
   
operator Agg() const {...}
};

vector
<Agg> v;
v
.emplace(Value());

This has a well-defined meaning right now: this will call `operator Agg` to get an `Agg`, then copy that `Agg` into the `vector`. It does the equivalent of `Agg(Value())`.

If we changed the initialization routine to use `is_aggregate` by itself, then the meaning of this code changes. Since `Agg` is an aggregate, it would use list-initialization: `Agg{Value()}`. [dcl.init.list]3.1 doesn't apply, nor does 3.2. So 3.3 applies: performing aggregate initialization. And that is ill-formed, since `Value` is not convertible to an `int`. Therefore, the code is ill-formed.

I'm not suggesting we make a backwards-incompatible change ;)

My suggestion is to ask both questions:

template<typename T, typename ...Args>
auto initialize(Args &&...args)
{
   
if constexpr(is_constructible_v<T, Args...>)
       
return T(std::forward<Args>(args)...);
   
else if constexpr(is_aggregate_v<T>)
       
return T{std::forward<Args>(args)...};
   
else
       
static_assert(false, "Cannot initialize with parameters");
   
end
}

So if it is constructible through the arguments, then it calls the constructor. If it's not constructible, but the type is an aggregate, then it will perform list-initialization, which will always invoke aggregate initialization (since the type wasn't constructible via `args`, [dcl.init.list]/3.1 cannot possibly apply. And 3.2 can't apply, since `args` can't be a string literal). And if neither is true, then it cannot be initialized with those parameters at all.

The idea is simply to do what LWG 2089's current resolution is, except that it has these properties:

vector<vector<int>> v;
v
.emplace_back(5, 3);    //Valid, like before.
v
.emplace_back(5, 3, 2); //Still ill-formed, as it was before
v
.emplace_back(std::initializer_list<int>{5, 3, 2}); //Perfectly fine

I will admit to this though: the only reason to have `is_aggregate` is for cases like this: for choosing to use list-initialization only on types that actually are aggregates, rather than depending on the vagaries of [over.match.list]/1.1.

Indeed, the lack of such a trait is the only reason why I this version of `initialize` myself ;)

Ville Voutilainen

unread,
Dec 31, 2016, 7:01:58 PM12/31/16
to ISO C++ Standard - Future Proposals
On 1 January 2017 at 00:36, Nicol Bolas <jmck...@gmail.com> wrote:
>> The general problem is that it's impossible for a wrapper to convey
>> initialization really-perfectly.
>
>
> "Impossible" is a very strong word ;)

Right, I meant as far as the current language allows, and I'm fully
aware that the language
can be changed. :)

> But if we expand things a bit, if we allow the type of a parameter to
> statically control the form of initialization:
>
> template<typename T, typename ...Args>
> T initialize(Args ...args)
> {
> return T(std::forward<Args>(args)...);
> }
>
> template<typename T, typename ...Args>
> T initialize(std::construct_init_t, Args ...args)
> {
> return T(std::forward<Args>(args)...);
> }
>
> template<typename T, typename ...Args>
> T initialize(std::list_init_t, Args ...args)
> {
> return T{std::forward<Args>(args)...};
> }

Yeah, such ideas have been floated around occasionally. Such
approaches might even eventually
end up appearing as library tools, who knows. To what extent existing
rules can be changed or
bent is another matter, as I'm sure you understand.

> Now, I'm not saying I want to see this standardized. I'd much rather use the
> ad-hoc approach outlined in LWG 2089. But it should be something to think
> about.

Yep, duly noted, I think the interested parties are somewhat aware of
such ideas.

>> Let me elaborate: which is the right question to ask:
>> - whether a type is an aggregate?
> Well, asking `is_aggregate` alone is a non-starter:
>
> struct Agg
> {
> int i;
> };
>
> class Value
> {
> public:
> operator Agg() const {...}
> };
>
> vector<Agg> v;
> v.emplace(Value());
>
> This has a well-defined meaning right now: this will call `operator Agg` to
> get an `Agg`, then copy that `Agg` into the `vector`. It does the equivalent
> of `Agg(Value())`.
>
> If we changed the initialization routine to use `is_aggregate` by itself,
> then the meaning of this code changes. Since `Agg` is an aggregate, it would
> use list-initialization: `Agg{Value()}`. [dcl.init.list]3.1 doesn't apply,
> nor does 3.2. So 3.3 applies: performing aggregate initialization. And that
> is ill-formed, since `Value` is not convertible to an `int`. Therefore, the
> code is ill-formed.
>
> I'm not suggesting we make a backwards-incompatible change ;)

Well, +1. :) As much as some of us would like to change/tweak the
C++11 rules, the horses and the cows
are far in the field, and calling them home is.. ..challenging. :)

> My suggestion is to ask both questions:
>
> template<typename T, typename ...Args>
> auto initialize(Args &&...args)
> {
> if constexpr(is_constructible_v<T, Args...>)
> return T(std::forward<Args>(args)...);
> else if constexpr(is_aggregate_v<T>)
> return T{std::forward<Args>(args)...};
> else
> static_assert(false, "Cannot initialize with parameters");
> end
> }
>
> So if it is constructible through the arguments, then it calls the
> constructor. If it's not constructible, but the type is an aggregate, then
> it will perform list-initialization, which will always invoke aggregate
> initialization (since the type wasn't constructible via `args`,
> [dcl.init.list]/3.1 cannot possibly apply. And 3.2 can't apply, since `args`
> can't be a string literal). And if neither is true, then it cannot be
> initialized with those parameters at all.

That approach is promising. In case Marshall, Jonathan and I manage to
forget any of this,
please do keep tabs on the follow-ups.

Tony V E

unread,
Jan 1, 2017, 1:25:37 AM1/1/17
to ISO C++ Standard - Future Proposals
Maybe we just need to allow functions to be called with {} as well as () and overloaded on bracket type. So initialize(a,b,c) vs initialize{a,b,c}.

:-)


Sent from my BlackBerry portable Babbage Device
  Original Message  
From: Ville Voutilainen
Sent: Saturday, December 31, 2016 7:01 PM
To: ISO C++ Standard - Future Proposals
Reply To: std-pr...@isocpp.org
Subject: Re: [std-proposals] Whatever happened to LWG 2089?
--
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.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/CAFk2RUZYLZfM8g%2Bim4OYkAa73NF4jpXvWfJ7u_6YSezs_ZhyAQ%40mail.gmail.com.
Reply all
Reply to author
Forward
0 new messages