type v = val; // #1
type v(val); // #2
type v = {val}; // #3
type v{val}; // #4
type v = val;
int v = 0; // (a)
auto v = 0; // (b)
auto v = /* initial value */;
auto v = type{val}; // or any other constructor arguments, including none
auto v = type(val); // Most vexing parse not a problem
auto v = val; // a single copy construction
auto v = type{val}; // can be a single copy construction
auto v = type(val); // can be a single copy construction
auto v = type{}; // can be just default construction
auto v = std::atomic<int>{};
struct foo
{
foo() { std::cout << "constructing foo\n"; }
~foo() { std::cout << "destructing foo\n"; }
foo(foo const&) = delete;
foo& operator=(foo const&) = delete;
foo(foo&&) = delete;
foo& operator=(foo&&) = delete;
};
int main()
{
foo f;
}
// Output:
// constructing foo
// destructing foo
int main()
{
auto f = foo{};
}
struct foo
{
foo() { std::cout << "constructing foo\n"; }
~foo() { std::cout << "destructing foo\n"; }
foo(foo const&) { std::cout << "copy constructing foo\n"; }
foo& operator=(foo const&) { std::cout << "copy assigning foo\n"; return *this; }
foo(foo&&) { std::cout << "move constructing foo\n"; }
foo& operator=(foo&&) { std::cout << "move assigning foo\n"; return *this; }
};
int main()
{
auto f = foo{};
}
// Output:
// constructing foo
// destructing foo
int i = 0; // an int
int i = int{0}; // identical to first line (does narrowing check)
int i = int(0); // identical to first line (no narrowing check)
auto i = 0; // identical to first line
auto i = int{0}; // identical to first line (does narrowing check)
auto i = int(0); // identical to first line (no narrowing check)
int i = int{}; // default construction
auto i = int{}; // identical to above
// Same semantics with types with UDLs
std::string s{"foo"}; // std::string
auto s = std::string{"foo"}; // identical to first line
auto s = "foo"s; // using a UDL, (effectively) identical
std::string s = std::string{}; // default construction
auto s = std::string{}; // identical to above
// Non-copyable, non-movable types, too
std::atomic<int> i = 0; // an std::atomic<int>
std::atomic<int> i = std::atomic<int>{0}; // identical to first line
std::atomic<int> i = std::atomic<int>(0); // identical to first line
auto i = std::atomic<int>{0}; // identical to first line
auto i = std::atomic<int>(0); // identical to first line
std::atomic<int> i = std::atomic<int>{}; // default construction
auto i = std::atomic<int>{}; // identical to above
auto a = std::atomic<int>{42};
std::atomic<int> b = a; // won't compile (non-copyable)
auto b = a; // same
std::atomic<int> b = std::move(a); // won't compile (non-movable)
auto b = std::move(a); // same
auto func() -> std::atomic<int>;
std::atomic<int> b = func(); // won't compile (non-movable)
auto b = func(); // same
std::atomic<int> b = std::atomic<int>{func()}; // Won't compile, because the
// construction on the right
// is invalid. Would work if
// func() return an int.
auto b = std::atomic<int>{func()}; // same
// Familiar behaviours (that already assume elision) are unchanged
std::vector<int> v = std::vector<int>{}; // default construction
auto v = std::vector<int>{}; // same
std::vector<int> v = std::vector<int>{2, 3}; // list construction { 2, 3 }
auto v = std::vector<int>{2, 3}; // same
std::vector<int> v = std::vector<int>(2, 3); // constructs vector { 3, 3 }
auto v = std::vector<int>(2, 3); // same
// If you stick with the policy to always use auto on the left,
// even the initializer list issue that gnaws on Meyers becomes clear:
auto i = 0; // i is int (what else would it be?)
auto i = {0}; // i is initializer_list<int> (what else would it be?)
auto i = int{0}; // i is int (what else would it be?)
// It only gets weird if you do:
int i = 0; // Clear
int i = {0}; // What? You want to assign an initializer_list<int> to an int?
// Well, I guess it works if there's only one int in the list...
int i = int{0}; // Clear, but redundant
// Plus all the headaches mentioned in N4014
// But that's a style issue.
// I am talking about, for example. either of
tuple<int> t = tuple<int>{0};
auto t = tuple<int>{0};
// I am not talking about:
tuple<int> t = {0};
// Which is what N4014 was about
The point is that EWG disagreed with the part which you claimed cannot be
reasonably be interpreted as anything else but default-constructing. We didn't
want to remove the semantic check for copy/move even for the case where the
copy/move is elided. That semantic check is what makes your example
be rejected.
auto t2 = t1
auto t2 = T{t1};
T t2{t1};
T t2 = t1;
T t2 = T{t1};
// etc., and of course traits, and eventually concepts
On 2015–09–10, at 7:27 AM, Mark A. Gibbs <indi.in....@gmail.com> wrote:Scott Meyers's recent post
on initialization has brought up a long-time pet peeve of mine: defining new objects. My beef is not that there are multiple ways to do it, it's that there is no single way that Just Works(tm) everywhere, and does the right thing, with robust syntax. The "Almost Always auto" style proposed by Herb Sutter comes close, but there are some gotchas with it. It seems to me there is a very simple way to remove those gotchas, but I've never seen it proposed.
I'm going to start by explaining why I've chosen AAA as the basis for this proposal. If you're already down with AAA, you can skip to the tl;dr at the bottom.
Meyers enumerates 4 different ways to declare and initialize a new variable. With "type" being either a type name or "auto", and "val" being a lvalue of type "type", they are:type v = val; // #1
type v(val); // #2
type v = {val}; // #3
type v{val}; // #4
Let's just write #2 off immediately, because of the most vexing arse - pardon the typo.
#4 is deeply problematic, because the behaviour has changed between C++14 and C++17. So let's drop that, too.
#3 has different behaviour whether "type" is an actually a type name or "auto". So let's put that aside for the moment.
That leaves us with:type v = val;
Which, using int, can be either:
int v = 0; // (a)
auto v = 0; // (b)
(a) is fine, but somewhat redundant and occasionally dangerous - if the type isn't the same on the left and right, you may end up with a conversion that may be expensive, or may lose information.
So that brings us down to (b), and the whole train of logic above turns to be a reiteration of the "Almost Always auto" (AAA) argument. For those who aren't familiar, the AAA argument is that variables should always be defined using the pattern:auto v = /* initial value */;
The benefits of this include:
- No unexpected conversions.
- Impossible to have an uninitialized variable.
- Consistent with other modern constructs, easy to read, hard to misunderstand.
Generally speaking, if you already have a value and you just want to initialize a copy or move it, you can just use:
auto v = val;
But if you don't have a value, or if you do but you want to be explicit about the type, you can use:auto v = type{val}; // or any other constructor arguments, including none
But not so fast....
As Meyers noted, this pattern has one case where it doesn't work: types that are non-copyable and non-movable.
The reasoning that even though the standard allows the move (or copy) construction to be elided, it doesn't require it. And compilers have to pretend they're still going to do it, even though they're not. That is why this won't compile:auto v = std::atomic<int>{};
But this restriction is silly and pedantic. There is no way the line above can reasonably be interpreted as anything else but "I want v to be a default constructed std::atomic<int>”.
And if anyone wants to try to argue that someone might actually want to do a default construction then move construction when writing that, I call shenanigans. Because that's not what they're going to get - in any compiler of note - and the standard blesses this.
Put yourself in the shoes of a C++ newbie who's just learning the language. Wanting to explore constructors and destructors, ze writes this simple class:
On 2015–09–10, at 10:53 AM, David Krauss <pot...@gmail.com> wrote:No, MVP happens only with an empty argument list.
>
> still doesn't work because even there, a semantic check for copy/move
> is performed.
Right. I don’t favor that stylistically, but I’ll take it if it can’t be surgically separated from non-movable factory functions which are a hole in the language.
[[emplace]] Widget makeWidget() { return Widget(); }
[[emplace]] auto x = makeWidget();
[[emplace]] auto x = std::atomic<int>{};
static_assert(is_movable<Widget>::value == false);
Widget makeWidget() <=; //Declare an inplace return function that must do RVO.
auto w <= makeWidget(); //inplace construct w
auto i <= std::atomic<int>{}; //inplace construct i
On 2015–09–11, at 12:34 AM, Matthew Fioravante <fmatth...@gmail.com> wrote:I would love to have factory functions for non-movable types. It seems that with return value optimization, most of the work is alreadydone. Unfortunately I think adding support would require new syntax of some kind.
On Thursday, September 10, 2015 at 1:24:56 AM UTC-4, David Krauss wrote:
>
> still doesn't work because even there, a semantic check for copy/move
> is performed.
Right. I don’t favor that stylistically, but I’ll take it if it can’t be surgically separated from non-movable factory functions which are a hole in the language.
I would love to have factory functions for non-movable types.
It seems that with return value optimization, most of the work is alreadydone. Unfortunately I think adding support would require new syntax of some kind.Maybe an attribute [[emplace]]. This would force RVO, enabling support for "returning" a non-movable object and also allowing the programmer to enforce that RVO is used for functions which return movable types.
[[emplace]] Widget makeWidget() { return Widget(); }
[[emplace]] auto x = makeWidget();
[[emplace]] auto x = std::atomic<int>{};Of course with that approach people might just end up sprinkling [[emplace]] in all of their functions to optimize and increase compatibility...Another option could be a new "inplace" initialization operator to use instead of =. I don't have a full specification for such a thing but it might look something like this:
static_assert(is_movable<Widget>::value == false);
Widget makeWidget() <=; //Declare an inplace return function that must do RVO.
auto w <= makeWidget(); //inplace construct w
auto i <= std::atomic<int>{}; //inplace construct iSuch an operator could even be used for value semantic types to specify the specific intent of direct construction vs copying. Op <= is a strawman for bikeshedding, but I think it could work since the comparison <= doesn't make sense in a new variable declaration or function signature.In general the C++ language is designed around value semantics (i.e. construct and move) but has pretty poor support for inplace construction of immobile types. There are many (I would argue artificial) holes in the language and library when you need to work with immobile types.
--
---
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/.
On 2015–09–11, at 4:37 AM, Richard Smith <ric...@metafoo.co.uk> wrote:struct Immovable {Immovable(int, int);
Immovable(const Immovable&) = delete;void operator=(const Immovable&) = delete;};Immovable factory() {return {1, 2};
}auto &&val = factory();
On Thu, Sep 10, 2015 at 9:34 AM, Matthew Fioravante <fmatth...@gmail.com> wrote:
On Thursday, September 10, 2015 at 1:24:56 AM UTC-4, David Krauss wrote:
>
> still doesn't work because even there, a semantic check for copy/move
> is performed.
Right. I don’t favor that stylistically, but I’ll take it if it can’t be surgically separated from non-movable factory functions which are a hole in the language.
I would love to have factory functions for non-movable types.struct Immovable {Immovable(int, int);Immovable(const Immovable&) = delete;void operator=(const Immovable&) = delete;};Immovable factory() {return {1, 2};}auto &&val = factory();
On 2015–09–11, at 4:37 AM, Richard Smith <ric...@metafoo.co.uk> wrote:struct Immovable {Immovable(int, int);This should be explicit. After all, it’s not a value-semantic class.
Immovable(const Immovable&) = delete;void operator=(const Immovable&) = delete;};Immovable factory() {return {1, 2};Omitting Immovable from this line is exactly wrong.
}auto &&val = factory();Lifetime extension only works for scoped variables.
struct caps {Immovable member { factory() };Immovable * ptr = new Immovable { factory() };};
On 2015–09–11, at 5:29 AM, Richard Smith <ric...@metafoo.co.uk> wrote:
On Thu, Sep 10, 2015 at 1:56 PM, David Krauss <pot...@gmail.com> wrote:This should be explicit. After all, it’s not a value-semantic class.That is not the criterion I use for this decision. The question is, does the pair of ints describe the value of the object (implicit constructor) or does it describe a set of inputs to a /computation/ that will produce the value of the object (explicit constructor)?
That's just, like, your opinion, man. =)
Yes. Guaranteed copy elision from temporaries would provide a complete solution (and I might present a paper on that topic at Kona). Then we don't need those fancy tricks, and we can instead just write the natural code:
Immovable factory() {return Immovable{1, 2}; // no copy here}auto val = factory(); // no copy here either
On 2015–09–11, at 5:29 AM, Richard Smith <ric...@metafoo.co.uk> wrote:On Thu, Sep 10, 2015 at 1:56 PM, David Krauss <pot...@gmail.com> wrote:This should be explicit. After all, it’s not a value-semantic class.That is not the criterion I use for this decision. The question is, does the pair of ints describe the value of the object (implicit constructor) or does it describe a set of inputs to a /computation/ that will produce the value of the object (explicit constructor)?That's just, like, your opinion, man. =)That was meant as a reference my first message. Value semantics are a bit subjective, but they’re anti-correlated with a deleted move constructor.If you can’t have this:Immovable do_ = Immovable{ 2, 2 };then how can this be allowed?Immovable do_ = { 2, 2 };The only conceptual wiggle room is the fleeting idea that the list itself is a value.Yes. Guaranteed copy elision from temporaries would provide a complete solution (and I might present a paper on that topic at Kona). Then we don't need those fancy tricks, and we can instead just write the natural code:Hooray! Let me know if I can help.
Hmm, should that be allowed with copy-initialization, or should auto val{ factory() } be required?Immovable factory() {return Immovable{1, 2}; // no copy here}auto val = factory(); // no copy here either
On 2015–09–11, at 10:03 AM, Richard Smith <ric...@metafoo.co.uk> wrote:With the approach I'm intending to follow, that will be valid (and that seems necessary, given that 'return', argument passing, braced init lists, and so on all use copy-initialization).
The problem with copy-initialization is that it’s sometimes used with no equals sign, such as in passing, returning, and throwing. The language treats it as a conservative choice when it’s impossible to guarantee that two values are the same object. However, this guarantee can always be made when passing, throwing, or returning a prvalue. The language/ABI interface is interfering with the programmer’s stated intentions.
Elision hasn’t been mentioned yet :( .
Writing random code from no semantic premises, and guessing at meaning from the perspective of a naive person, is not part of language design.The solution to this problem is that newbies shouldn’t write non-value-semantic classes. Deleting the move constructor is a red flag, indicating that programming assistance is needed.(Snip — the newbie did not seek help. This seldom ends well in any language.)
And BTW, I agree with Ville’s that your proposal is closely aligned with N4014. They both result from blaming the problems of non-value-semantic classes on the equals sign instead of the class. The equals sign is a safety mechanism to protect you from thorny types; getting rid of its significance will only open the gates to more nastiness.
atomic<int> x = {};
atomic<int> x = atomic<int>{};
// This is not an operation on values
using type = other_type;
// Neither is this
namespace ns = verbose::name::space;
// Perl
my $x = initializer;
// OCaml, Rust
let x = initializer;
// JavaScript, Swift
var x = initializer;
// C++, conceptually
auto x = initializer;
type x = type{}; // (direct) default construction
type x = type(); // same
// Both of the above work for *all* types that have default constructors,
// regardless of whether it's explicit or not, regardless of whether the type
// is movable and/or copyable. It Just Works(tm).
type x = type{...}; // (explicit, direct) list initialization
type x = type(...); // (explicit, direct) non-list initialization
// Both of the above work for *all* types that have the constructor with the
// given signature, regardless of whether it's explicit or not, regardless of
// whether the type is movable and/or copyable. As with today, an initializer
// list constructor gets priority in the first case, and a non-initializer-list
// constructor gets priority in the second case. It Just Works(tm).
type x = lvalue; // copy construction (presumably)
type x = rvalue; // move construction (presumably)
type x = std::move(lvalue); // move construction (presumably)
type x = factory_function(); // move construction (presumably)
// The semantics of the above don't change - they only work for types that
// allow copy/move construction. These all Just Work(tm) for value types.
type x = 0; // int
type x = {0}; // int
// The semantics of the above don't change.
// The type names don't need to be identical, they just need to resolve to the
// same type.
using alias = type;
alias x = type{}; // (direct) default construction
alias x = type(); // same
alias x = type{...}; // (explicit, direct) list initialization
alias x = type(...); // (explicit, direct) non-list initialization
auto x = type{}; // (direct) default construction
auto x = type(); // same
// Both of the above work for *all* types that have default constructors,
// regardless of whether it's explicit or not, regardless of whether the type
// is movable and/or copyable. It Just Works(tm).
auto x = type{...}; // (explicit, direct) list initialization
auto x = type(...); // (explicit, direct) non-list initialization
// Both of the above work for *all* types that have the constructor with the
// given signature, regardless of whether it's explicit or not, regardless of
// whether the type is movable and/or copyable. As with today, an initializer
// list constructor gets priority in the first case, and a non-initializer-list
// constructor gets priority in the second case. It Just Works(tm).
auto x = lvalue; // copy construction (presumably)
auto x = rvalue; // move construction (presumably)
auto x = std::move(lvalue); // move construction (presumably)
auto x = factory_function(); // move construction (presumably)
// The semantics of the above don't change - they only work for types that
// allow copy/move construction. These all Just Work(tm) for value types.
auto x = 0; // int
auto x = {0}; // std::initializer_list<int> (with one element, that is 0)
// which is different from using explicit type (but still clear)
// The semantics of the above don't change.
// No issues with type aliasing
template <typename T>
auto func()
{
// I expect this template function to only be instantiated for types that are
// movable and/or copyable, and rather than expressing this via traits or
// concepts, I'm relying on the current interpretation of the following lines:
T t1 = T{};
auto t1 = T{};
}
// assuming type_1 is non-copyable and non-movable
using type_2 = type_1;
template <typename T, typename U>
auto func()
{
// I expect this template function to do a default construction of a temporary
// U object, then a conversion construction of a T using that temporary.
// In fact, I *INSIST* on this behaviour, and even use a special compiler, or
// compiler switches, to make it happen.
T t = U{};
}
func<type_1, type_2>();
But if you don't have a value, or if you do but you want to be explicit about the type, you can use:
auto v = type{val}; // or any other constructor arguments, including none
On 2015–09–11, at 12:02 PM, Mark A. Gibbs <indi.in....@gmail.com> wrote:Most of my best students are those who took the initiative to figure things out in the language via experimentation. When you can't do that - or worse, as in this situation, when experimentation leads you confidently to the wrong answer - that means the language simply doesn't make sense. I don't think the correct answer to that dilemma is "you shouldn't experiment". The problem is not the student, it's the language.
Nothing rules out consulting an expert, of course, but the point is that you have to know when to consult an expert, and what questions to ask. You can't simply walk up to a language expert and say "just tell me everything". All experts and help communities expect you to do your homework and come prepared only with those questions you can't figure out on your own. (And in this case, trying to figure it out on your own will lead you to the wrong answer. Thus, we have a problem.)
Actually, making the language make more sense to beginners is my primary motivation for proposing this change (but also, easier generic programming).
I would love to be able to tell the people I teach, on day 1 with no hemming or hawing or qualification: "This is how you create a new object in C++. Just do this. Auto, name, equals, type, braces, semicolon. Done. Want to initialize with a specific value? Stick it in the braces. Yes, there are other ways that you'll certainly see in other people's code, and in time - when we cover more topics - we'll talk about the various pros, cons, and gotchas. For now, just do this. It works." Then I can move on to teaching them actually useful things, and not have them come up to me a couple days later, "um, I tried to use this lock guard, and…".
Simple things should be simple. There doesn't need to be a goblin lurking in every single corner of the language.
And BTW, I agree with Ville’s that your proposal is closely aligned with N4014. They both result from blaming the problems of non-value-semantic classes on the equals sign instead of the class. The equals sign is a safety mechanism to protect you from thorny types; getting rid of its significance will only open the gates to more nastiness.
Okay, I think I'm cluing in to what you and Ville are saying. When referring to the relationship to N4014, you are not actually referring to the actual content of the paper (or the note on why it was rejected), but rather to what I assume is a discussion that went on behind the scenes. That discussion was not just on the specific issue (of explicitness), but a broader discussion about protecting the purity of separation between value types and non-value types.
And I gather what you're saying is that you want the equals sign to be the line of separation between value types and non-value types - that is, when you're using an equal sign, you must be working with a value type.
Does that correctly summarize what you and Ville are getting at?
Assuming so, I have to disagree with the reasoning. I don't disagree with maintaining a bright line of separation between value types and non-value types (let's just go with VTs and NVTs) - what I disagree with is that the equal sign is worth defending for this cause. I would say that battle is over, and purity lost. Here's why:atomic<int> x = {};
That works, but this doesn't:atomic<int> x = atomic<int>{};
So logically that means that the first statement is not really constructing an atomic on the right - it's (in essence) assigning (which is really a conversion construction) the (empty) list object on the right to the atomic on the left.
But if that's so, then why doesn't it work with types with an explicit default constructor?
The fact that it fails when the constructor is explicit implies that there must be (implicitly) an atomic<int> construction going on on the right. I mean, you can't sanely argue that whatever is being constructed on the left isn't explicitly an atomic<int>, right?
The difference between direct and non-direct initialization give us two options:Type x = foo(); //This will compile only if there is implicit conversion between result of foo() and Type
Type x(foo()); //Will compile when there is explicit conversion from result of foo() and Type
Ironically the poeple that are promoting auto everywhere syntax are given argument about lack of unintended conversion as argument for using it. While use ETTI (auto x = type(expr)) acutally introduce possibility of more bogus one. Just think of the Herb Sutter example:
auto x = unique_ptr<Base>{createDerived()};
It is working correctly in case when createDerived() is retruning unique_ptr<Derived>. But what if it will start to return raw pointer to class to object that has lifetime handled eslewhere? The syntax:
unique_ptr<Base> x = createDerived();
Will catch such change at compile time.
int get_count();
// Then later get_count() is changed to return a long. Or std::size_t.
int n = get_count(); // This will fail to detect the change. Bug reports of
// counts of -1234 objects ensue.
auto n = int{get_count()}; // This will detect the issue at compile time.
template <typename T, typename U>
auto as(U&& u) -> T
{
return {u};
}
auto s = as<std::chrono::seconds>(get_timeout());
// or if you prefer a different name:
auto s = implicitly_as<std::chrono::seconds>(get_timeout());
auto s = safely_as<std::chrono::seconds>(get_timeout());
I didn’t say not to experiment, I said that you get into trouble when you lose value semantics. The problem with the example is that its “experiment” just seems to be a random jumble of keywords, and then you say that the result is surprising.
An experiment is something that tests a hypothesis, and the example never said what the student was after.
In this case, the question would be, “why doesn’t the equals sign work”?
And the answer would be, “the right-hand side doesn’t behave as a value.”
auto&& x = std::atomic<int>{};
The flip side of regular syntax with loose semantic constraints is getting unexpected meaning from templates. Some things aren’t supposed to compile.
There’s a language for that: Java. (Except, scalars in Java are new objects without using the new keyword.)
Assuming so, I have to disagree with the reasoning. I don't disagree with maintaining a bright line of separation between value types and non-value types (let's just go with VTs and NVTs) - what I disagree with is that the equal sign is worth defending for this cause. I would say that battle is over, and purity lost. Here's why:atomic<int> x = {};
That works, but this doesn't:atomic<int> x = atomic<int>{};
Because the second example isn’t equivalent to the first.
Arguably this should work too:atomic< int > x = 0;I do think it’s a bit silly to convert to a temporary before initializing the named variable, but I can’t comment further without digging into the history of the language.
atomic<int> a = atomic<int>{0};
True, you’re proposing to guarantee elision of that copy. But copy elision was never intended as a pillar of the language. Things are supposed to work without it; it’s not a failsafe bypass around the lvalue-to-rvalue conversion.
On 2015–09–12, at 9:48 AM, Mark A. Gibbs <indi.in....@gmail.com> wrote:On Friday, 11 September 2015 04:11:43 UTC-4, David Krauss wrote:I didn’t say not to experiment, I said that you get into trouble when you lose value semantics. The problem with the example is that its “experiment” just seems to be a random jumble of keywords, and then you say that the result is surprising.
That is saying "don't experiment", because experimenting means trying things that you don't know whether they'll work or not,
and learning from the discovery of what happens. Okay, perhaps you're not saying "don't experiment ever", but you are saying "don't experiment with non-value types". Of course, the catch is that the beginner won't know that rule, and even if they do they won't know what non-value types are, so they won't know where they can or can't experiment. So, basically, "don't experiment”.
Anyway, if it's just that you think the experimentation example wasn't realistic, I can easily provide you with a realistic one. I've seen it happen plenty of times.
An experiment is something that tests a hypothesis, and the example never said what the student was after.
Now you're just trying to be pedantic, but I can do that, too. A science experiment is one that involves hypothesis testing, but even then not all science experiments involve hypotheses: there is such a thing as "exploratory data analysis". Experimentation in the general sense is merely "trying shit to see what happens". The latter type may be non-rigorous, but it is very often the first stage in an investigation that will be followed up by the former type. Great scientific discoveries often start with "hm, I wonder what happens if…".
(Also, in the original example, there was a hypothesis being tested. Specifically, that one of the four special member functions copy/move construct/assign was being used in that initialization statement. The experiment was putting markers in the four functions to reveal which. You could consider that a "closed testing procedure" that tests multiple hypotheses - in this case, 4, each with its own null hypothesis - simultaneously.)
In this case, the question would be, “why doesn’t the equals sign work”?
Actually, the question is not "why doesn't the equal sign work", because "atomic<int> a = {};" works but "atomic<int> a = atomic<int>{};" doesn't. Clearly the equal sign does work. The question is why doesn't it work when I write the type twice, even though the braces on the right appear to be implying a default constructed atomic on the right
that is being "put" in the atomic on the left - or aliased by the variable being declared on the left. And this behaviour appears to be what that construct implies, because that's what apparently happens according to "atomic<int> a = 0;”
(doesn't work because non-copy/non-movable) and "tuple<int> t = {0};" (doesn't work because construction on the right is implicit).
I suggest that you are making a mistake by trying to interpret "type x = type{...};" as an assignment between two values. Firstly, that seems a suspicious argument right from the get go, because you don't have two values. In fact, you have none before, then one after.
And unlike all other forms of initialization, in the interim you have two objects that were constructed out of whole cloth right there in the statement, unobservably (because of elision). (This is different from the cases of "type x = val;" and "type x = func_yielding_rvalue();", because in both those two cases, the RHS value is constructed elsewhere (though possibly elided).) The argument that you really, really want this ghost object that never really exists seems weak.
Secondly, the language refutes that interpretation. If a non-copy/non-move type has a constructor that takes an int, but no assignment operators that take an int, this still works: "type x = {0};”.
How can that be if this is actually a transfer of values? The type has no way to take an int (or an initializer list) except by construction, so it must therefore be constructing on the right... but that can't be, because the type is non-copy/non-movable, so if it were constructed on the right, it can't possibly be transferred to the left.
Therefore there cannot be two values in that statement - there must only be one, the object on the right constructed with the int on the left. (Of course, the language then turns around and screws you again, if that constructor is explicit. As I keep saying, I think you're barking up the wrong tree trying to argue for consistency in construction statements. That ship has sailed.)
And the answer would be, “the right-hand side doesn’t behave as a value.”
Yet this works:auto&& x = std::atomic<int>{};
So clearly the claim that "=" only works with value types just doesn't hold water.
"=" may be the dividing line between value types and non-value types in general code - when it is actually being used as an assignment. But in the context of initialization, it isn't, at least not consistently.
The flip side of regular syntax with loose semantic constraints is getting unexpected meaning from templates. Some things aren’t supposed to compile.
But as I pointed out, that argument doesn't really apply to declarations of the form "type x = type{...};". Nobody writes code like that with the expectation that it is the only thing keeping non-copy/non-move types out the door. Almost 20 years of C++ and 10 years of teaching it,
I have never seen that done (and I've seen some crazy shit). Anybody who does that is being "clever" to the point of frustrating future maintenance of the code, so there is no reason the standard should have to bless that behaviour. There are better ways to prevent generic code from compiling in the face of a non-value type. We just don't need this particular case.
Also, I'm not proposing "loose" semantic constraints. I'm proposing very rigorous constraints... just constraints that are different from the existing ones in one specific context (which are specious).
You're making it sound like I'm proposing throwing all distinction between value and non-value types out - or perhaps removing the ability for "=" to distinguish in all cases. That's not so. I'm talking about taking one... specific... instance - just "type x = type{...};", and not even "type x = val;" or "type x = func();", those should still fail in the absence of copy/move ops - in a context where there are already lots of special cases and exceptions (initialization), and for the gain of ending the madness of having no way to construct a new object that works for all types. I think we can let this one go.
There’s a language for that: Java. (Except, scalars in Java are new objects without using the new keyword.)
I realize you're invoking Java as the exemplar of a language that prefers simplicity and consistency at the expense of flexibility, expressiveness and power... exactly what we don't want C++ to become. But frankly, if a bad language has a good idea... I'm a-gonna take it. I don't care if the rest of the design in that language is misguided - if it has something that works well, and it can be done in C++ without sacrificing anything important... yeah, I'll steal from Java. I don't have so much pride that I would refuse a good idea just because of its source.
I didn't claim they were equivalent. In fact, I obviously realize they aren't because the whole point of my proposal is to make them equivalent.
I pointed out that the claim that "=" implies value semantics is false. Atomics don't have value semantics, yet you can do "atomic<int> a = {};”.
If it were really true that "=" really did imply value semantics in all cases, then I would have to acquiesce - you would be right, and maintaining a clear wall distinction between value types and non-value types is more important than convenience. But it doesn't always imply value semantics. There are already exceptions and gotchas... especially in the context I'm focused on. Since the wall is already full of holes, the argument for adding one more that makes things really convenient and consistent suddenly has merit.
Arguably this should work too:atomic< int > x = 0;I do think it’s a bit silly to convert to a temporary before initializing the named variable, but I can’t comment further without digging into the history of the language.
So why shouldn't this?:atomic<int> a = atomic<int>{0};
What you're saying is that you think that it's reasonable that "atomic<int> a = 0;" should be interpreted as "atomic<int> a{0};”
, and not "atomic<int> a = atomic<int>{0};" (as it currently is). I agree - I see no sane reason why it shouldn't. It's obvious what you really mean, and there's really no reason a temporary has to be constructed then transferred to the named variable.
So if that's reasonable, why is interpreting "atomic<int> a = atomic<int>{0};" as "atomic<int> a{0};" so crazy? It's the exact... same... reasoning: It's obvious what you really mean, and there's really no reason a temporary has to be constructed then transferred to the named variable.
Remember, "atomic<int> a = {0};" is already interpreted as "atomic<int> a{0};”.
"{0}" just by itself is an initializer_list<int>,
yet atomic<int> has no initializer_list<int> constructor, so that has to be what the interpretation of what's happening is - the "{0}" is not "an initializer list", but rather "the initializer list for the atomic being constructed". This is already a context were the "normal" rules of "=" just don't apply.
What I'm proposing is basically just interpreting "atomic<int> a = atomic<int>{0};" as "atomic<int> a atomic<int>{0};", then dropping the redundant repetition of the type. (But, of course, ONLY in the case where the type actually is repeated.)
True, you’re proposing to guarantee elision of that copy. But copy elision was never intended as a pillar of the language. Things are supposed to work without it; it’s not a failsafe bypass around the lvalue-to-rvalue conversion.
Yes, and I am proposing to change that - in this one, specific syntactic construct. That's why elision is mentioned in the topic.
You're right, the current state of affairs is that elision is optional in all cases, even in the case that I'm focused on. My argument is if we change that rule just in this specific case, to make the elision effectively required - and "required" to the point that there is no moving at all, even conceptually - we gain:The only purposes behind keeping the requirement that the type be copy/movable are either a) to maintain language purity wrt the distinction of value/non-value types, or b) because it is a practical way to actually distinguish between value/non-value types in generic code.
- guaranteed efficiency (as opposed to almost-certain but not required efficiency)
- and a practical solution to the initialization madness that works in all cases (though may not do precisely what you specifically need in a specific case, in which case you still have the other options).
For (a), I say that goal is already a lost cause, in this context. There are so many peculiarities in the interpretations of the various construction syntaxes, that this doesn't really seem the right place to be adamant about drawing this line.
For (b), I say: nobody does it for that purpose, nobody would understand that's what the intention was if it was actually used for that purpose, there are several other - better - ways of achieving the same goal, and finally, it just don't work anyway. There are more requirements to having value semantics than just being copy/movable, so a type that does not have value semantics but is copy/movable would pass the bar.
Given that the syntax doesn't actually distinguish anything but whether the type is non-copy/non-movable or not - it doesn't actually serve the purpose of detecting value semantics - and that construction in general is already a context where some things that look like copies/moves are not actually, I think we can get away with repurposing it, and gaining clarity and sanity in one of the most basic and important parts of the language: constructing objects.
No initialization syntax is perfect. But the difference between your example and mine is that mine is a plausible update to a code base to accommodate growth, whereas yours is really just a really dumb refactoring decision that someone hoped that compiler would spare them from the consequences of. The "auto" version catches the plausible modification, but misses the terrible refactoring job. The non-"auto" version catches the really dumb change that should never happen, but fails to catch the plausible modification. Frankly, I'd prefer the "auto" result, in practice. Oh, also, the "auto" version will crash the very first time the code that puts the result of createDerived() in a unique_ptr gets called - basically day 1 of testing - and debug mode will give you a message about the double-delete... meanwhile the bug in the non-"auto" version could go for years without being detected, unless and and until there happens to be a case when the result of get_count() is too large, which may only happen sporadically and be hard to reproduce. Advantage: auto.
Back to your example, relying on the semantics of "type x = factory_function();" is a bad idea. Unless you put a comment next to it explicitly explaining that that's what you intend, you risk someone coming along and thinking that "type x{factory_function()};" is a better idea, because it prevents narrowing, or something similar. The smart thing to do is this:
template <typename T, typename U>
auto as(U&& u) -> T
{
return {u};
}
auto s = as<std::chrono::seconds>(get_timeout());
// or if you prefer a different name:
auto s = implicitly_as<std::chrono::seconds>(get_timeout());
auto s = safely_as<std::chrono::seconds>(get_timeout());
That will catch unwanted explicit conversions... and it will catch narrowing (and if you don't want that, drop the braces)... and it is self-documenting in code. And if you don't use the auto there, you have to repeat the type, which is a bad idea. For auto, I think that's game, set, and match.
Perhaps those of us who want to "force" everyone to use the "shiny new auto" have given this a bit more thought than you give us credit for.
Anyway, to be clear, I am not proposing a way to initialize objects that will solve every single possible problem imaginable. I am just proposing a way that will work for every type, and do a sensible thing in all cases. It may not work precisely the way you need it to for your specific instance, in which case you can use a more specific form.
auto x = int[]{ 1, 2, 3 };
Just want to post a question I asked on stackoverflow where a comment was that my code wasn't future proof. It was about a class that could not be created on the heap, relying on a deleted copy and move constructor and the fact that you can capture such a variable in a temporary:
http://stackoverflow.com/questions/34948403/
This proposal would destroy that use case, if it'd go through and actually open up a problem for me if there was no way to suppress the behaviour for a specific class.
I have no proposition on how to tell the compiler that a specific class would not allow copy/move elision but if it would, to be consistent, I'd make it forbidden to do so, even in the cases that are currently allowed by the standard. But there should be a way to forbid/restrict - see private/protected move and copy constructors.
class foo stackonly
{
//
};
--
---
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 https://groups.google.com/a/isocpp.org/group/std-proposals/.
Wouldn't that choice of words imply introducing the very concept of a stack in the standard? As far as I know, it's not there currently (it's a good thing in some ways, and an annoyance in others).
--