Discussion:
[std-discussion] List/Aggregate/Direct initialization and user defined conversion operators
Hannes Hauswedell
2017-05-18 13:47:00 UTC
Permalink
Hi everyone,

I have stumbled about this peculiar behaviour of clang and gcc:
https://godbolt.org/g/6IhJlV

That is, if we have A and B, and B has a user defined conversion operator
for A, than both of these should work:

B b;
A a1(b); // (1)
A a2{b}; // (2)

Why should they work?
Common sense:
* IIANM conversion used to be independent of whether it happens at the
target (via constructor) or via the source (user defined conversion
operator).
* with the introduction of explicit conversion operators in c++11 this
has become even more so
Standard:
* (1) is plain old behaviour
* (2) http://eel.is/c++draft/dcl.init.list#3.8

Do they work?
* (1) of course
* (2) works if A is a non-aggregate class type, because
http://eel.is/c++draft/dcl.init.list#3.8
* (2) fails if A is an aggregate class type, because the compiler picks
http://eel.is/c++draft/dcl.init.list#3.3 instead of
http://eel.is/c++draft/dcl.init.list#3.8

From my POV this behaviour is confusing, because we recommend people use
brace-initialization more routinely than parenthesis-initialization now and
aggregate initialization should add flexibility, right? :)
Or did I miss something and there is some benefit to (2) not working for
aggregate types?

Thanks and regards,
Hannes Hauswedell

P.S: for reference here the small discussion started over at cppreference
about this:
http://en.cppreference.com/w/Talk:cpp/language/direct_initialization
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Hannes Hauswedell
2017-05-18 14:36:58 UTC
Permalink
Post by Hannes Hauswedell
Hi everyone,
https://godbolt.org/g/6IhJlV
[snip]

I see that something similar is being discussed in "Revisiting core issue
1758", but the issue I am reporting is independent of "explicit"ness so I
am not sure if they are related that closely.

Best regards,
Hannes
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Richard Smith
2017-05-18 21:43:39 UTC
Permalink
Post by Hannes Hauswedell
Hi everyone,
https://godbolt.org/g/6IhJlV
That is, if we have A and B, and B has a user defined conversion operator
B b;
A a1(b); // (1)
A a2{b}; // (2)
Why should they work?
* IIANM conversion used to be independent of whether it happens at the
target (via constructor) or via the source (user defined conversion
operator).
* with the introduction of explicit conversion operators in c++11 this
has become even more so
* (1) is plain old behaviour
* (2) http://eel.is/c++draft/dcl.init.list#3.8
Do they work?
* (1) of course
* (2) works if A is a non-aggregate class type, because
http://eel.is/c++draft/dcl.init.list#3.8
* (2) fails if A is an aggregate class type, because the compiler picks
http://eel.is/c++draft/dcl.init.list#3.3 instead of
http://eel.is/c++draft/dcl.init.list#3.8
From my POV this behaviour is confusing, because we recommend people use
brace-initialization more routinely than parenthesis-initialization now and
aggregate initialization should add flexibility, right? :)
Not everyone recommends this approach. [Note: what follows is my opinion,
not necessarily that of anyone else on the committee.] As a general rule,
when a language offers multiple tools to perform an operation, it's a good
idea to pick the weakest tool that achieves the goal: prefer a named cast
to a C-style cast, prefer implicit conversion to a cast, prefer a loop to a
goto, and so on. The reason is that the weaker tools are safer, and they
give the reader more insight into your intent.

The same applies to initialization. The weakest form of initialization is
copy-initialization, so the recommended form would be

A a3 = b;

Direct-list-initialization (your option 2) is in most ways the strongest
form of initialization (it tries to support most initialization that direct
non-list initialization would support, and more), so following the strength
argument, it should only be used when other forms are too weak or otherwise
inapplicable, which is seldom the case.


Another perspective is to think of the semantics expressed to a reader by
each form of initialization:

X a = b;

says: "the value of a is the value of b", whatever that means. That might
imply a conversion, but should not imply that the value of a is *computed
from* b in some sense. For instance, this is nonsense:

std::vector<std::string> v = 5;

... because there is no such thing as a vector of strings whose value is
abstractly the number 5, but this is meaningful:

std::complex<double> d = 5;

... because there is a complex<double> whose value is abstractly 5.
Corollary: constructors that construct the value of the object to *be* the
value of their argument should not be explicit. Constructors that *compute*
the value of the object from their arguments should be explicit. (And
likewise for explicit conversion operations: those say that there is some
natural way to *compute* a B from an A, whereas an implicit conversion
operator says that a value of type A can simply be *interpreted as* a value
of type B in some natural way.)

X a(b, c, d);

says: "the value of a is computed from the arguments b, c, d". This use of
parentheses mirrors the language syntax for a function call, which again
expresses a computation that might have side-effects. It makes sense that
this syntax can call explicit constructors, because that matches the
meaning of the syntax. And it also makes some sense that this syntax can
call implicit constructors, because one obvious way to compute a value of
type X from an operand is to merely interpret the operand as directly
specifying the value. For example,

std::vector<std::string> v(5, "foo");

... *does* make some sense, as a way of saying "compute a vector of strings
from the arguments 5, "foo" ", and it's not entirely unreasonable for this
to compute that the result is a vector containing 5 "foo"s.


List-initialization follows the same rules:

X a = {b, c, d};

says "the value of a is the value of the 3-tuple b, c, d" (in some obvious
sense). So:

std::complex<double> d = {0, 1};

is a reasonable way to write 0.0 + 1.0i, and

std::vector<std::string> v = {"foo", "bar", "baz"};

is a reasonable way to write a vector containing those three strings. This
also gives a good intuition for why narrowing conversions are disallowed
here: when we're specifying the value of some object *is* some other value,
we don't want that value to be truncated before it's used in the
initialization.

Based on the above, it's somewhat of a judgement call whether

X a;
X b = {a};

should work at all or not. Personally, I don't think it should, since it
obscures the intent of the code to allow such things, but the direction of
the committee appears to be to allow it.


Finally:

X a{b, c, d};

... is direct-initialization (no = sign), so we're computing the value of
an X from a 3-tuple b, c, d. But because we're given a 3-tuple, not just 3
individual arguments, this might also be specifying the trivial computation
of a value from that value itself (as before with list-initialization) --
that is, that the value *is* in some sense {a, b, c}. Naturally, this leads
to conflicts when the two interpretations mean different things. For
instance,

A a2{b}; // your (2)

might mean that we aggregate-initialize an A, with the first member
initialized from b. Or it might mean that we compute the value of A from b
(like your (1)). Or it might mean that we just take b as the value of the A
object (which is what you wanted). So the language makes a guess, and in
your case it guesses wrong. This is not dissimilar from what happens here:

template<typename T> void f() {
const size_t how_many = 10;
std::vector<T> v{how_many, T()};
}
void g() { f<T>(); }

... where the initialization could mean "a vector of the value 20, 10
times" or could mean "a vector containing the integers 10 and 20". So, this
syntax tries to do everything, does not communicate the intent to the
reader or to the compiler, will sometimes pick the wrong interpretation as
a result, and thus should be avoided whenever other techniques are
available.

Or did I miss something and there is some benefit to (2) not working for
Post by Hannes Hauswedell
aggregate types?
Thanks and regards,
Hannes Hauswedell
P.S: for reference here the small discussion started over at cppreference
http://en.cppreference.com/w/Talk:cpp/language/direct_initialization
--
---
You received this message because you are subscribed to the Google Groups
"ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an
Visit this group at https://groups.google.com/a/is
ocpp.org/group/std-discussion/.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Nicol Bolas
2017-05-19 03:02:41 UTC
Permalink
So, this syntax tries to do everything, does not communicate the intent to
the reader or to the compiler, will sometimes pick the wrong interpretation
as a result, and thus should be avoided whenever other techniques are
available.
While I agree with your conclusion, I disagree with the reasoning by which
you arrive at it.

In your interpretation, you make a distinction between "computed from" and
"interpreted/converted from". I don't feel that this distinction really
matters to most users.

Generally speaking, I don't care *how* `A a = b;` works. Whether `b` is
actually an `A` or whether it uses implicit conversions, that's all an
irrelevance. I simply expect it to initialize `a` using the value of `b`,
whatever that means.

The implicit/explicit distinction doesn't really matter for a declaration
like `A a = b;`. It matters when you're initializing objects where the type
of the object being initialized is not locally visible. Function arguments,
return values, and so forth. Yes, the rules of copy initialization prevents
`A a = b;` from calling explicit conversions, but those cases are far more
likely to be false positives than catching user error.

If `B` is implicitly convertible to `A`, then that means that `B`s can be
treated as `A`s to some degree. If they not implicitly convertible, then
they can't be treated like them. So if a user wants to try to do so, we
want to make sure that the user is keenly aware of exactly what they've
asked the system to do. So we make them explicitly type out the typename.

Whether you're using list initialization or not doesn't really matter. It's
whether the typename is there or not; that's what clears things up for the
user.

Where list initialization goes off the reservation is in cases like the OP
has here: where the user had a clear expectation of one kind of behavior,
and the standard gave them a different one.

So-called "Uniform Initialization" was created under the belief that users
could manifest all forms of object initialization simply by providing a
list of values. That a list of values is 100% of the information the
compiler needs to be able to initialize any type of object in every fashion
of initialization that object can undergo.

That belief is incorrect. And that incorrect belief lead to a flawed
mechanism.

Conceptually, I think the most useful way to think of list initialization
in its current form is that it is a tool to initialize an "aggregate" of
some form. That is, you're initializing an object with a list of values,
and this object is essentially *nothing more* than this list of values.
This covers genuine aggregates and pretty much anything with an
`initializer_list` constructor.

If the values you prove an object's initialization are (broadly speaking)
not the object's state, then you should not use list initialization.

This view of list initialization makes the use of {} for value
initialization make sense. Any object that can be value initialized can
conceptually be created from no values. So doing `Typename{}` to value
initialize a type (mainly to avoid the Most Vexing Parse) is a perfectly
reasonable thing to do for every (value initailizable) type.

This interpretation of list initialization also makes one of the classic
failures of "Uniform Initialization" actually make sense: `vector<int>`:

vector<int> i(5, 2);
vector<int> j{5, 2};

`i` is not storing 5 and 2 into the `vector`; those are just constructor
parameters. `j` is storing 5 and 2 into the `vector`. So if we stick to
this interpretation of list initialization, we will never try to use {} to
reach the size+value constructor of `vector<int>`.

Now, let's apply it to the OP's code.

A a1(b);
A a2{b};

`a1` means to construct an `A` using the value of `b`. `a2` means to store
a value of `b` within `a`. If `A` is not an aggregate or aggregate-like
type, list initialization is the wrong form of initialization.

The purpose of this interpretation of list initialization is to keep you
from encountering the failings of the syntax. You have to express your
intent in the code: are you initializing an aggregate/sequence-of-values,
or are you constructing something?

Of course, one of the issues is that list initialization can be invoked in
a number of places where constructor initialization cannot (easily).
Default member initializers being the most important place where {} is
allowed and () is not (to avoid parsing ambiguities). Which is what makes
the current list initialization's failings so annoying.

But that's just my take on the whole thing.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Richard Smith
2017-05-19 03:29:05 UTC
Permalink
Post by Nicol Bolas
So, this syntax tries to do everything, does not communicate the intent
to the reader or to the compiler, will sometimes pick the wrong
interpretation as a result, and thus should be avoided whenever other
techniques are available.
While I agree with your conclusion, I disagree with the reasoning by which
you arrive at it.
In your interpretation, you make a distinction between "computed from" and
"interpreted/converted from". I don't feel that this distinction really
matters to most users.
Generally speaking, I don't care *how* `A a = b;` works. Whether `b` is
actually an `A` or whether it uses implicit conversions, that's all an
irrelevance. I simply expect it to initialize `a` using the value of `b`,
whatever that means.
I think this is a question of style and idiom. If you want to work in a
codebase where the value of type A can be an arbitrary computation based on
the value of 'b', you are of course free to do so, but I don't think your
coding style would communicate as well to a reader as code that only uses
this form of initialization when the value of the A object can be described
simply as being in some sense the same as the value of b. Given:

Image x = "foo.png";

I would be really surprised if that goes to disk, loads the file, parses
it, and produces some kind of buffer containing the pixel data (rather than
just being a type that, say, holds a filename). But for

Image x("foo.png");

loading the image from disk seems a lot more reasonable to me (and indeed,
I would probably even expect it to do something like that, not just hold
onto the filename).

But sure, there are many different "accents" in which you could choose to
speak C++, and your choice of accent will affect how easy or hard it is for
others to understand your code. I'm not going to tell you you're wrong just
because you speak in a different accent from me :)

The implicit/explicit distinction doesn't really matter for a declaration
Post by Nicol Bolas
like `A a = b;`. It matters when you're initializing objects where the type
of the object being initialized is not locally visible. Function arguments,
return values, and so forth. Yes, the rules of copy initialization prevents
`A a = b;` from calling explicit conversions, but those cases are far more
likely to be false positives than catching user error.
If `B` is implicitly convertible to `A`, then that means that `B`s can be
treated as `A`s to some degree. If they not implicitly convertible, then
they can't be treated like them. So if a user wants to try to do so, we
want to make sure that the user is keenly aware of exactly what they've
asked the system to do. So we make them explicitly type out the typename.
Whether you're using list initialization or not doesn't really matter.
It's whether the typename is there or not; that's what clears things up for
the user.
My view: if the typename is not there, then you only get to perform
conversions that more or less preserve the idea of the value, and you don't
get to perform an arbitrary implicit construction that could produce an
entirely different value. But that's not the whole story; you still get to
make the conversion versus computation choice even if the typename is
present.

Where list initialization goes off the reservation is in cases like the OP
Post by Nicol Bolas
has here: where the user had a clear expectation of one kind of behavior,
and the standard gave them a different one.
So-called "Uniform Initialization" was created under the belief that users
could manifest all forms of object initialization simply by providing a
list of values. That a list of values is 100% of the information the
compiler needs to be able to initialize any type of object in every fashion
of initialization that object can undergo.
That belief is incorrect. And that incorrect belief lead to a flawed
mechanism.
Conceptually, I think the most useful way to think of list initialization
in its current form is that it is a tool to initialize an "aggregate" of
some form. That is, you're initializing an object with a list of values,
and this object is essentially *nothing more* than this list of values.
This covers genuine aggregates and pretty much anything with an
`initializer_list` constructor.
If the values you prove an object's initialization are (broadly speaking)
not the object's state, then you should not use list initialization.
This view of list initialization makes the use of {} for value
initialization make sense. Any object that can be value initialized can
conceptually be created from no values. So doing `Typename{}` to value
initialize a type (mainly to avoid the Most Vexing Parse) is a perfectly
reasonable thing to do for every (value initailizable) type.
This interpretation of list initialization also makes one of the classic
vector<int> i(5, 2);
vector<int> j{5, 2};
`i` is not storing 5 and 2 into the `vector`; those are just constructor
parameters. `j` is storing 5 and 2 into the `vector`. So if we stick to
this interpretation of list initialization, we will never try to use {} to
reach the size+value constructor of `vector<int>`.
Now, let's apply it to the OP's code.
A a1(b);
A a2{b};
`a1` means to construct an `A` using the value of `b`. `a2` means to store
a value of `b` within `a`. If `A` is not an aggregate or aggregate-like
type, list initialization is the wrong form of initialization.
The purpose of this interpretation of list initialization is to keep you
from encountering the failings of the syntax. You have to express your
intent in the code: are you initializing an aggregate/sequence-of-values,
or are you constructing something?
Of course, one of the issues is that list initialization can be invoked in
a number of places where constructor initialization cannot (easily).
Default member initializers being the most important place where {} is
allowed and () is not (to avoid parsing ambiguities). Which is what makes
the current list initialization's failings so annoying.
But that's just my take on the whole thing.
I basically agree with you on all of that :)
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Hannes Hauswedell
2017-05-19 14:59:02 UTC
Permalink
Post by Richard Smith
Post by Nicol Bolas
Generally speaking, I don't care *how* `A a = b;` works. Whether `b` is
actually an `A` or whether it uses implicit conversions, that's all an
irrelevance. I simply expect it to initialize `a` using the value of `b`,
whatever that means.
I think this is a question of style and idiom. If you want to work in a
codebase where the value of type A can be an arbitrary computation based on
the value of 'b', you are of course free to do so, but I don't think your
coding style would communicate as well to a reader as code that only uses
this form of initialization when the value of the A object can be described
Image x = "foo.png";
I would be really surprised if that goes to disk, loads the file, parses
it, and produces some kind of buffer containing the pixel data (rather than
just being a type that, say, holds a filename). But for
Image x("foo.png");
loading the image from disk seems a lot more reasonable to me (and indeed,
I would probably even expect it to do something like that, not just hold
onto the filename).
This is a good example and I agree with it completely, but it just pertains to
explicit vs implicit, or not? If {} is advertized as the replacement-where-
possible for (), than

Image x{"foo.png"};

will be read just as

Image x("foo.png");

Or would you argue that since {} disallows narrowing it should also disallow
explicit conversions, therefore making

Image x{"foo.png"};

be like

Image x = "foo.png";
?

I am not sure I agree, but in any case A a{b}; should work if b has an non-
explicit user defined conversion operator, or not? Right now it doesn't if A
is an aggregate type....

Thanks for all your feedback!

Regards,
Hannes
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Hannes Hauswedell
2017-05-19 14:58:17 UTC
Permalink
Post by Nicol Bolas
Now, let's apply it to the OP's code.
A a1(b);
A a2{b};
`a1` means to construct an `A` using the value of `b`. `a2` means to store
a value of `b` within `a`. If `A` is not an aggregate or aggregate-like
type, list initialization is the wrong form of initialization.
Well, that's a very general statement. I will make a point in favor if {}
initialization below, but my main argument is that {} is here already, it
performs the above behaviour for non-aggregate class types, it just doesn't do
so for aggregate types which is very unexpected.

And my main question was, whether
1) I am wrong in expecting it; or
2) the standard's wording is wrong w/ regard to the original intention; or
3) the compilers are wrong in implementing the standard

From my superficial reading of the sections I lean towards 1) or 2) :)

On a more general note, I will try to make my context clearer and why I do
"like" brace initialization:

I am currently redesigning a bioinformatics template library based on C++17,
the Concepts TS and the Ranges TS/range-v3-library. Relying on brace
initialization adds a lot of flexibility that I otherwise couldn't get,
especially in the context of concepts, because I can formulate my requirements
agnostic of the actual implementation, i.e. aggregate initialization saves me
from writing biolerplate constructors, and uniform initialization makes it
very easy to use aggregates and non-aggregates in the same places.
While it is true that there can be ambiguities of using brace-initialization
everywhere, I have yet to encounter them in a context that is designed fully
around Modern C++ principles.

Best regards,
Hannes
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Nicol Bolas
2017-05-19 15:27:51 UTC
Permalink
Post by Hannes Hauswedell
Post by Nicol Bolas
Now, let's apply it to the OP's code.
A a1(b);
A a2{b};
`a1` means to construct an `A` using the value of `b`. `a2` means to
store
Post by Nicol Bolas
a value of `b` within `a`. If `A` is not an aggregate or aggregate-like
type, list initialization is the wrong form of initialization.
Well, that's a very general statement. I will make a point in favor if {}
initialization below, but my main argument is that {} is here already, it
performs the above behaviour for non-aggregate class types, it just doesn't do
so for aggregate types which is very unexpected.
And my main question was, whether
1) I am wrong in expecting it; or
2) the standard's wording is wrong w/ regard to the original intention; or
Post by Hannes Hauswedell
3) the compilers are wrong in implementing the standard
From my superficial reading of the sections I lean towards 1) or 2) :)
On a more general note, I will try to make my context clearer and why I do
I am currently redesigning a bioinformatics template library based on C++17,
the Concepts TS and the Ranges TS/range-v3-library. Relying on brace
initialization adds a lot of flexibility that I otherwise couldn't get,
especially in the context of concepts, because I can formulate my requirements
agnostic of the actual implementation, i.e. aggregate initialization saves me
from writing biolerplate constructors, and uniform initialization makes it
very easy to use aggregates and non-aggregates in the same places.
While it is true that there can be ambiguities of using
brace-initialization
everywhere, I have yet to encounter them in a context that is designed fully
around Modern C++ principles.
I would love to have an initialization form that could do what () would
normally do, but for cases where that wouldn't work, it would perform
aggregate initialization (and if that doesn't work, it would be
ill-formed). Indeed, we have a standard library issue (#2089) that asks for
`emplace`-like functions to provide that exact behavior.

But list initialization *does not do that*.

That's why I redefine the meaning of list initialization as I have. The
meaning I have explained *guarantees* that you will never encounter
ambiguities when using the syntax with that intent. That meaning may not
have been what Stroustrup originally intended for list initialization, but
original intent doesn't matter next to the feature we actually got.

List initialization does not work as a default form of initialization.
*Especially* in template code, where you may or may not know the types of
what's out there. The sooner you recognize that, the better off you will be.

So the answer to your question is #1: yes, you are wrong to expect this
from list initialization as it is currently defined. And the standards
committee seems decidedly unwilling to improve it, so that's how it's going
to have to be.
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Hannes Hauswedell
2017-05-19 14:57:39 UTC
Permalink
Thanks for the detailed answers first of all!
Post by Richard Smith
Post by Hannes Hauswedell
From my POV this behaviour is confusing, because we recommend people use
brace-initialization more routinely than parenthesis-initialization now and
aggregate initialization should add flexibility, right? :)
Not everyone recommends this approach. [Note: what follows is my opinion,
not necessarily that of anyone else on the committee.] As a general rule,
when a language offers multiple tools to perform an operation, it's a good
idea to pick the weakest tool that achieves the goal: prefer a named cast
to a C-style cast, prefer implicit conversion to a cast, prefer a loop to a
goto, and so on. The reason is that the weaker tools are safer, and they
give the reader more insight into your intent.
The same applies to initialization. The weakest form of initialization is
copy-initialization, so the recommended form would be
A a3 = b;
Direct-list-initialization (your option 2) is in most ways the strongest
form of initialization (it tries to support most initialization that direct
non-list initialization would support, and more), so following the strength
argument, it should only be used when other forms are too weak or otherwise
inapplicable, which is seldom the case.
I am not sure I agree with the strength<->weakness order. Copy initialization
allows only implicit conversions and direct (non-list) initialization allows
explicit conversions, too, so the order is clear, but if we introduce {} it
becomes less clear, e.g.

int i = 2.5; // works
int j{2.5}; // doesn't because narrowing

Otoh of course more things do work, like aggregate initialization which is why
I want to use it...
Post by Richard Smith
Based on the above, it's somewhat of a judgement call whether
X a;
X b = {a};
should work at all or not. Personally, I don't think it should, since it
obscures the intent of the code to allow such things, but the direction of
the committee appears to be to allow it.
Then you agree that the behaviour described in my original post is not as
*intended* by the committee even if it does represent the current wording?
Post by Richard Smith
X a{b, c, d};
... is direct-initialization (no = sign), so we're computing the value of
an X from a 3-tuple b, c, d. But because we're given a 3-tuple, not just 3
individual arguments, this might also be specifying the trivial computation
of a value from that value itself (as before with list-initialization) --
that is, that the value *is* in some sense {a, b, c}. Naturally, this leads
to conflicts when the two interpretations mean different things. For
instance,
A a2{b}; // your (2)
might mean that we aggregate-initialize an A, with the first member
initialized from b.
It is true that the exact behaviour *could* be ambiguous, but in my case it
clearly isn't. As you document in other places this ambiguity already appears
in other cases, and under the circumstances I would assume a specific
behaviour that is not performed.
Post by Richard Smith
Or it might mean that we compute the value of A from b
(like your (1)). Or it might mean that we just take b as the value of the A
object (which is what you wanted). So the language makes a guess, and in
your case it guesses wrong.
Well, IMHO it doesn't need to guess, because there is no ambiguity...
Post by Richard Smith
template<typename T> void f() {
const size_t how_many = 10;
std::vector<T> v{how_many, T()};
}
void g() { f<T>(); }
... where the initialization could mean "a vector of the value 20, 10
times" or could mean "a vector containing the integers 10 and 20". So, this
syntax tries to do everything, does not communicate the intent to the
reader or to the compiler, will sometimes pick the wrong interpretation as
a result, and thus should be avoided whenever other techniques are
available.
I agree that this is unfortunate, but these constructors stem from a time
where we didn't have brace initialization. I would hope that people designing
libraries (STL2?) take this into consideration nowadays. Many of the more
obscure constructors can be replaced with new functionality that is very
clear, e.g. std::vector<int> v = view::repeat_n(5, 2);

Best regards,
Hannes
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Hannes Hauswedell
2017-05-19 11:24:21 UTC
Permalink
Thanks for the detailed answers first of all!
Post by Richard Smith
Post by Hannes Hauswedell
From my POV this behaviour is confusing, because we recommend people use
brace-initialization more routinely than parenthesis-initialization now and
aggregate initialization should add flexibility, right? :)
Not everyone recommends this approach. [Note: what follows is my opinion,
not necessarily that of anyone else on the committee.] As a general rule,
when a language offers multiple tools to perform an operation, it's a good
idea to pick the weakest tool that achieves the goal: prefer a named cast
to a C-style cast, prefer implicit conversion to a cast, prefer a loop to a
goto, and so on. The reason is that the weaker tools are safer, and they
give the reader more insight into your intent.
The same applies to initialization. The weakest form of initialization is
copy-initialization, so the recommended form would be
A a3 = b;
Direct-list-initialization (your option 2) is in most ways the strongest
form of initialization (it tries to support most initialization that direct
non-list initialization would support, and more), so following the strength
argument, it should only be used when other forms are too weak or otherwise
inapplicable, which is seldom the case.
I am not sure I agree with the strength<->weakness order. Copy initialization
allows only implicit conversions and direct (non-list) initialization allows
explicit conversions, too, so the order is clear, but if we introduce {} it
becomes less clear, e.g.

int i = 2.5; // works
int j{2.5}; // doesn't because narrowing

Otoh of course more things do work, like aggregate initialization which is why
I want to use it...
Post by Richard Smith
Based on the above, it's somewhat of a judgement call whether
X a;
X b = {a};
should work at all or not. Personally, I don't think it should, since it
obscures the intent of the code to allow such things, but the direction of
the committee appears to be to allow it.
Then you agree that the behaviour described in my original post is not as
*intended* by the committee even if it does represent the current wording?
Post by Richard Smith
X a{b, c, d};
... is direct-initialization (no = sign), so we're computing the value of
an X from a 3-tuple b, c, d. But because we're given a 3-tuple, not just 3
individual arguments, this might also be specifying the trivial computation
of a value from that value itself (as before with list-initialization) --
that is, that the value *is* in some sense {a, b, c}. Naturally, this leads
to conflicts when the two interpretations mean different things. For
instance,
A a2{b}; // your (2)
might mean that we aggregate-initialize an A, with the first member
initialized from b.
It is true that the exact behaviour *could* be ambiguous, but in this case it
clearly isn't. As you document in other places this ambiguity already appears
in other cases, and under the circumstances I would assume a specific
behaviour that is not performed.
Post by Richard Smith
Or it might mean that we compute the value of A from b
(like your (1)). Or it might mean that we just take b as the value of the A
object (which is what you wanted). So the language makes a guess, and in
your case it guesses wrong.
Well, IMHO it doesn't need to guess, because there is no ambiguity...
Post by Richard Smith
template<typename T> void f() {
const size_t how_many = 10;
std::vector<T> v{how_many, T()};
}
void g() { f<T>(); }
... where the initialization could mean "a vector of the value 20, 10
times" or could mean "a vector containing the integers 10 and 20". So, this
syntax tries to do everything, does not communicate the intent to the
reader or to the compiler, will sometimes pick the wrong interpretation as
a result, and thus should be avoided whenever other techniques are
available.
I agree that this is unfortunate, but these constructors stem from a time
where we didn't have brace initialization. I would hope that people designing
libraries (STL2?) take this into consideration nowadays. Many of the more
obscure constructors can be replaced with new functionality that is very
clear, e.g. std::vector<int> v = view::repeat_n(5, 2);

Best regards,
Hannes
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
'Johannes Schaub' via ISO C++ Standard - Discussion
2017-05-19 20:59:19 UTC
Permalink
Post by Hannes Hauswedell
Hi everyone,
https://godbolt.org/g/6IhJlV
That is, if we have A and B, and B has a user defined conversion operator
B b;
A a1(b); // (1)
A a2{b}; // (2)
Why should they work?
* IIANM conversion used to be independent of whether it happens at the
target (via constructor) or via the source (user defined conversion
operator).
Note that C++11 made it possible to directly bind temporaries to
references. Therefore the following will first try the conversion
function, because that's classified as a "direct binding"

struct A;
struct B { operator A(); };
struct A { A(B); };

A &&a = B();

I think this is well-formed code, despite that GCC and Clang reject it
as ambiguous (I may be wrong, but please show me why, in that case).
--
---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
Loading...