-
-
Notifications
You must be signed in to change notification settings - Fork 628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Placement new doesn't call implicit copy constructor #20950
Comments
Strictly speaking, there is no "implicit copy constructor" here (i.e., this is not an overload resolution failure)—but placement new should still perform copy initialization in this case. |
Well it can either perform a copy directly, or a copy constructor can be generated which will naturally be selected in all applicable constructs where a constructor may be called... I would do the latter for uniformity. |
This has nothing to do with placement new; the same error message happens without it:
The reason is the there is no implicit copy constructor in D. If there are no explicit constructors, the compiler will just do a bit copy. If there are explicit constructors, the compiler will select a constructor based on the overload rules. If there is no match, the compiler will not generate one that matches, it will just fail. The rationale is that overloading can sometimes be baffling as to which overload is selected. (Unfortunately, the overload rules are not as simple as I originally intended.) Compounding the confusion is if the compiler is generating another unintended invisible constructor and calling none of the explicit overloads. There is also the confusion about what kind of qualifiers would be on this invisible constructor. So the idea is once one goes down the road of making the struct non-POD with non-trivial constructors, one needs to finish the job and be explicit about it. |
I think the desired behavior here is for Arguably, it would be inconsistent for It is worth noting that |
These should not behave differently as far as construction goes:
They look the same, and behaving differently would be quite surprising. Changing the behavior of the non- When the user creates a constructor for a type, he is saying to the compiler, "I've got this, I know what I'm doing." The compiler shouldn't generate more hidden constructors - the existence of those hidden constructors can even break the semantics of his code (yes I know about Consider this:
and the user accidentally writes:
when he meant to write:
But the compiler doesn't detect that because it created an invisible D isn't for unnecessary syntax, but also it tries to avoid the risk of invisible bugs in the service of saving keystrokes. Besides, it's not at all difficult to write a copy constructor, and it's nice to be explicit about it. |
Okay, but right now, as in the code above: Test a;
Test b = a; // succeeds, performs copy-initialisation
new(b) Test(a); // fails, but should do exactly the same as the line above These need to do the same thing. Whatever the resolution, both lines of code should have identical behaviour. Likewise for this: Test b = Test(1,2,3); // perform a move initialisation of `b` (after user construction)
new(b) Test(Test(1,2,3)); // identical to the above line That's the very next line of code I'll write after this issue is resolved to test the same with move construction? Placement new should not have different behaviour. You appear to be overcomplicating it; the Test x = ...; // <-- whatever this expression does in the existing language
new(x) Test(...); // <-- this expression should be IDENTICAL to the line above; call the same function Ideally, the placement new should call the same code as variable initialisation; separate paths lead to bugs like this. I would try and refactor the initialisation logic into one place and both cases should call the exact same function to perform initialisation. They can't be different, the 2 expressions should be synonyms. This must be equivalent: // this must be identical...
Test b = void;
new(b) Test(...);
// to this
Test b = ...; I even suggested several months ago, because I was trying to emphasise the importance of the 2 expressions being identical; I suggested in terms of implementation. Test b = ...; // that this expression...
// could be lowered to this:
Test b = void;
new(b) Test(...); Where placement new is the only source of init logic in the whole compiler, and all other initialisation expressions lower to a placement new expression. It's a bit of a refactor, but I think it would increase semantic sanity, and improve fragility. |
Once there is a constructor for a struct, it is no longer POD, and the compiler will no longer generate constructors for it. As my original example showed, the compiler was doing the same thing for:
I.e. giving the same error, because AFAIK, the behavior of Our difference is that you wish the compiler to generate a default copy constructor for non-POD structs, and that is a mistake. (I don't remember what C++ did for that case, and if it did, it's still a mistake!) |
According to the language spec:
So, the struct in this example is POD. |
@pbackus you're right. I'll amend my comments with "has any user defined constructors" replacing "is POD". |
It's not though, did you try to reproduce this? Paste this: struct Test
{
int x, y, z;
this(int x, int y, int z) {} // explicit normal constructor
}
void main()
{
Test a;
Test b = a; // working: performs a copy as expected
// new(b) Test(a); // ERROR: doesn't compile!
} The initialisation compiles. Uncomment placement new, and you get a compile error.
No actually, this has nothing to do with what I think the behaviour should or shouldn't be. I'm reporting that the new expression produces an error where the init statement doesn't. |
They work the same. What you're seeing is:
are not the same thing. |
I'm confused. Test b = a; // initialise `Test` with the lvalue `a`
new(b) Test(a); // <-- equivalent expression; init `Test` from the lvalue `a` These expressions should be identical; initialise The rewrite you show isn't right, you're showing 2 different things side-by-side; let me fix it: Test b = Test(a); // initialise given a `Test` rvalue
new(b) Test(Test(a)); // <-- equivalent expression, init from a `Test` rvalue Those 2 expressions should be identical. So; maybe there's something wrong with how the new expression interprets its argument? |
Yeah okay, so that's the bug... the new expression is not interpreting the argument correctly. Again; the expression |
I don't want to explain to people why the
are different. They'll tell me that is a bug, and I'll be sympathetic! Just write the constructor :-/ |
This isn't about a missing constructor, it's a semantic mismatch.
No, you will have to explain the current state, because it is not reasonable. If I were to try and write some sort of pseudo code to show how it works, maybe this: Test b = a;
new (b) Test = (a); You've misunderstood what the token These must be equivalent expressions: Test b = a;
new(b) Test(a);
Test b = Test(a);
new(b) Test(Test(a));
Test b = int(10);
new(b) Test(int(10));
Test b = ...;
new(b) Test(...); I promise that if what I write here isn't exactly what the code does, it will immediately surprise anyone that tries to use this. Everyone who tried out placement new so far ran into this issue instantly. |
You can see how in both expressions, the word |
It's not unusual for people coming from C++ to try things the C++ way they're used to first. |
Pick a different grammar then. You seem to be stuck on that point, or you still misunderstand the actual point here... Situation: Test b = a; This expression initialises a variable. The issue is that not all variables are on the stack; it's essential that we can initialise a variable the same way given an address that may or may not be on the stack. I proposed: This should be read as:
It is NOT a constructor call, it is the type being specified for If you don't like the fact that the grammar looks like a constructor call, then please feel free to suggest a different grammar that you don't feel exposes that weakness. Personally, I think placement new declared this way is natural and precedented, and I think anything you could propose would violate the rule of least surprise. Everybody who's tried this so far as hit this wall instantly; they understood it to be as I say, and they were surprised when it was reinterpreted as a constructor call, because there may or may not be a constructor. This is for "initialisation", constructors are a subset of initialisation. |
|
Hmmm, I've never used The thing is, people usually don't complain about issues; they just fiddle with work-arounds until it works, and then they move on. That's not a good thing, but it's true. If they can't find a workaround, then they might make a noise. We have a fundamental operation; initialising a variable, and we can't express that operation, except in the one case where the variable is allocated on the stack. Take a look at There is a truckload of brittle and broken (fails various categories of optimisation, performs redundant work, etc) code in there handling this edge case. That rats nest is essentially trying to emulate the edge cases we're talking about here. Code like And then you say "I can't change anything, because everything breaks", and the reality is that logic like that in One way ore another, we must have the tool to initialise a variable. That shouldn't be subject to debate. |
You know, I reckon there's actually a decent chance that it wouldn't break Can we try it and see the scale of breakage? If there was ever an issue in the history of D, this issue is worthy of a breaking change. Or pick another grammar, and we start over with a new expression. |
This bickering over basic semantics suggests that placement new was merged far too prematurely. Perhaps a DIP would have helped avoid this? |
Super helpful contribution. I don't believe process would have caught this; the words on the DIP would have been the same words as we've been using to describe this work the whole time. |
Placement new is not the issue. The issue is the NewExpression syntax, which dates back 25 years. |
Yeah, I get that now... so, we either deviate from that spec for placement new, or we leave placement new how it is, and also add something beside it with different grammar? |
Can we find an expression like b = a; // an assignment, distinct from:
Ty b = a; // an initialisation Can new(b) = a; be similarly interpreted as an initialisation rather than an assignment? Ty a
new(a) are semantically equivalent; such that you can write: Ty a = b;
new(a) = b; And they are equivalent, both recipients of an initialisation. |
I still feel quite strongly that |
Personally I don't see anything wrong with allowing both types of struct S { int x; }
S original;
S* heapCopy = new S(original);
S stackCopy = void;
new(stackCopy) S(original); This has the advantage that D programmers wouldn't have to learn two separate sets of rules for stack and heap initialization—both This kind of uniformity is also very useful for writing generic code, where one might want to use the same initializer for either a stack or heap allocation depending on some compile-time condition. Given that this code currently does not compile, it should be a purely additive change, with minimal risk of breakage. |
In fact, here is an enhancement request from 2014(!) for allowing |
Nice find! |
The bug report 12918 doesn't really offer a solution, and doesn't address the problems I listed. But, since this issue has nothing to do with placement new, and the current behavior has been specified and in existence since the Dawn of Man, I suggest a proposal to address it be put in the d.d.ideas newsgroup and gather more input from the general community. |
I think this is just an oversight/bug... if you uncomment the explicit copy constructor, this works as it should.
The default constructor works, but for whatever reason, the implicit constructors aren't selected properly.
The text was updated successfully, but these errors were encountered: