Repository navigation
[BUG] Converting assignment generated from non-converting constructor #468
Description
Title: Converting assignment generated from non-converting constructor.
Description:
Relevant extracts from https://github.com/hsutter/cppfront/wiki/Cpp2:-operator=,-this-&-that
always talk about generating converting assignment from a converting constructor,
but cppfront currently generate converting assignment from an explicit constructor.
implicit: On a two-parameteroperator=function, writingoperator=: (implicit out this, /*...*/)defines a conversion function that can be used to perform implicit conversions to this type.
operator=is the generalized name for all value-setting functions, including the six combinations of { copy, move, converting } x { constructors, assignment operators }.Note: The concept of a converting assignment operator is first-class in Cpp2, and makes this 3x2 matrix of options symmetric. In Cpp1, it's common to write a converting constructor from some other type, but not an assignment operator from that other type, which leads to asymmetries like
mytype var = other;working butvar = other;not working.
- (A)ssignment, A1, A2, A3: If you write a copy or move or converting constructor, but not a corresponding copy or move or converting assignment operator, the latter is generated.
Note: Generating
inout this(assignment) fromout thisalso generates converting assignment from converting construction, which is a new thing. Today in Cpp1, if you write a converting constructor from another typeX, you may or may not write the corresponding assignment fromX; in Cpp2 you will get that by default, and it sets the object to the same state as the converting constructor fromXdoes.
Minimal reproducer (https://cpp2.godbolt.org/z/zzK9qc4Gx):
quantity: type = {
value: int = ();
operator=: (out this, val: int) = { value = val; }
}
main: () = {
x := quantity(0);
x = 1;
}
Commands:
cppfront -clean-cpp1 main.cpp2
clang++17 -std=c++2b -stdlib=libc++ -lc++abi -pedantic-errors -Wall -Wextra -Wconversion -I . main.cppExpected result:
An error at:
x = 1;Actual result and error:
Cpp2 lowered to Cpp1.
#include "cpp2util.h"
class quantity;
class quantity {
private: int value {};
public: explicit quantity(cpp2::in<int> val);
public: auto operator=(cpp2::in<int> val) -> quantity& ;
public: quantity(quantity const&) = delete; /* No 'that' constructor, suppress copy */
public: auto operator=(quantity const&) -> void = delete;
};
auto main() -> int;
quantity::quantity(cpp2::in<int> val)
: value{ val }
{}
auto quantity::operator=(cpp2::in<int> val) -> quantity& {
value = val;
return *this;
}
auto main() -> int{
auto x {quantity(0)};
x = 1;
}Output:
Program returned: 0
Activity
realgdman commentedon May 25, 2023
JohelEGP commentedon May 25, 2023
hsutter commentedon Jul 8, 2023
@realgdman
Besides the function body cases, in my own code I regularly use
{}to mean a default value. For example, for a function that returns a pointer,return {};andreturn nullptr;are equivalent. For a function that returns a string,return {};andreturn "";are equivalent. For many standard types, it also happens work today for "default assignment" (e.g.,my_vector = {};) to reset to essentially the default-constructed state, and so parallel "default construction" (e.g.,vector<int> my_vector{};). Maybe those are what you're seeing?hsutter commentedon Jul 8, 2023
@JohelEGP - thanks! I think this is intended behavior though... given your example type:
The first example is:
This is what I intended... I considered both lines to be equivalently "explicit" syntaxes. My view is that both are explicitly asking
xto be set to0or1. Note that with delayed initialization the above can be written equivalently like this:and with this version the
=equivalence is even more direct.My understanding of the danger of implicit conversions is when Cpp1 silently creates a temporary
quantityobject without any clue in the source that it's happening. In this case there are only explicitquantityobjects being constructed/assigned.Does that make sense?
JohelEGP commentedon Jul 9, 2023
Yes!
It definitely makes much more sense when portrayed with Cpp2,
especially
The use of the C++ standard term converting constructor in the wiki
made me think this should only apply to
operator=generated fromimplicit this.Now, how does one opt-out of this generation?
Types representing quantities don't want assignment from numbers, as that's type unsafe.
For example, the physical meaning of
x = 1changes silently under a refactor ofx's type.hsutter commentedon Jul 9, 2023
Opt-out is via
implicit.But I think what you're asking is maybe what if the user writes
y = Z();whereZhas a conversion tointand then the assignment toyworks? But then the implicit conversion is the conversion fromZtoint, so it's the responsibility of the author ofZto make that conversion explicit. (Which, if they're writing it in Cpp2, is the default...)JohelEGP commentedon Jul 9, 2023
That doesn't work: https://cpp2.godbolt.org/z/zTbj843v7.
I want the explicit assignment
x = 1to be an error.hsutter commentedon Jul 9, 2023
Oh, I see -- you want an opt-out of even generating the assignment?
Assuming you want to disable all mutation, not just assignment, one way is to make a
const quantityobject. Or to make thevaluemember beconst.If you want to allow mutating operations, and allow construction from
intbut not allow assignment fromint, this should work (but currently doesn't, that's just a bug I should fix):There's a bug here because I should not generate the converting assignment when it's user-written, but this would be the way to do it.
But let me ask: Given
x: quantity;, can you elaborate on whyx = 0; x = 1;should be an error only on the second statement? Andx = quantity(1);would still work? That is, I think you're asking for this:I'm trying to imagine a situation where that would be desirable... the reason why Cpp2 generates converting assignment from converting construction is that usually they should be symmetric and it's a surprise when Cpp1 class authors support construction from a different type but not assignment from that same type. Is there a use case you came across?
JohelEGP commentedon Jul 9, 2023
The use case is unit/quantity types.
x: metres = 1;definesxto be 1 m.If
x = 1;were allowed, that would setxto 1 m.But if the type of
xwere to change tox: millimetres = 1;.Suddenly, any following
x = 1changes from settingxto 1 m to settingxto 1 mm.So for added safety, unit libraries require the assignment from a quantity type and not a number:
x = metres(1); // OK, meaning doesn't change under a refactor of the type of `x`.hsutter commentedon Jul 9, 2023
Thanks. OK, so I see why you want to suppress assignment, and you'll soon be able to do that by writing a private converting assignment operator (that will work as soon as I fix the bug to not generate it if the user wrote it).
FWIW, this works nicely if you want the UDL-like suffix syntax, which in Cpp2 works for both literals and non-literals (below, an
intobject namedforty_two)... the following already works now:Note this way all the cases can use the same consistent syntax (though
x: quantity = 0;would still work, I'm just saying it's possible to use a single consistent style).I'll look at fixing the double generation bug so you can opt out and make
x = 1;assignment an error... thanks for the feedback and helping me notice the bug.JohelEGP commentedon Jul 9, 2023
Thank you.
What about clarifying or removing the uses of "converting"
from the quotes of the wiki article in the opening comment?
hsutter commentedon Jul 9, 2023
[Edited to add "default or converting"]
Ah, good point, I should address this phrase:
An explicit converting constructor is still a converting constructor, right? A converting constructor (and, now that we've generalized this with the other Issues in this area #375 #398 #450, any non-copy
operator=) can be explicit (the default) orimplicit.I've added a note to the
implicitbullet to try to clarify this:Does that help?
JohelEGP commentedon Jul 9, 2023
If it's
then only an
explicitconstructor can be a converting constructor.Not really.
It's the use of "converting" that led me to believe
that generated assignments should be tied to non-
explicitconstructors only.But you pointed out that's not actually the case.
So it turns out that whether an
operator='sthisparameter isimplicitor not has no effect on emitted assignments.So the use of "converting" in the wiki are misleading,
because what they state also apply to non-converting (
explicit) constructors.hsutter commentedon Jul 10, 2023
Ah, I didn't notice that "converting constructor" was already a Cpp1 standard term of art. The Cpp1 term seems like it would be better named an "implicitly converting constructor." A Cpp1
explicitconstructor still performs conversions, after all, just in a subset of contexts.In contrast, the Cpp1 standard uses the term "conversion function" whether it's implicit or explicit, then qualifies the term as needed, e.g., to say things like "a conversion operator may be explicit".
So restricting "converting constructor" to mean only a non-explicit one seems like inconsistent terminology between converting constructors and conversion operators, as well as inconsistent in that explicit constructors also perform type conversions just in a subset of contexts.
My inclination is to keep using the term consistently as I've been doing in Cpp2, but if it's confusing I may need to come up with a different term just to avoid the confusion.
JohelEGP commentedon Jul 10, 2023
I actually noticed that term when writing #468 (comment) 2 days ago.
But when I opened this issue,
for some reason,
I was convinced that all uses of "converting" in the wiki article
were referring to implicit conversions.
So yes, I think it's confusing to use "converting".
JohelEGP commentedon Jul 25, 2023
The opt-out by commit eac30d8 turns out to be rather noisy.
And because the solution only looks at the parameter's type,
it could fail in more involved signatures (e.g., using
requiresclauses).JohelEGP commentedon Jul 25, 2023
An alternative could be to discard the signature:
_ = operator=: (inout this, size: i32);.hsutter commentedon Jul 30, 2023
Thanks! Can you elaborate please...
Why isn't
privateenough?Yes, I'd put it in the initializer. Is there a reason it can't be?
I tried making this code warn using
-Wunused-valuebut didn't succeed in making it fire. Was that warning you saw, or something else?But assuming it was that: Perhaps it'd be useful to support giving a parameter the name
_and emitting[[maybe_unused]]in that case, which cppfront already does for local variables named_?JohelEGP commentedon Jul 30, 2023
To prevent it being accidentally invoked because it exists.
None in this case.
But that might not always be possible, or scale.
Or it might seem out of place.
Because its reason for being is as a workaround to something far off.
See also #305 (comment), where by "discard a parameter's name" I meant not emitting it.
_ = Elowers to(void) E,and
_ : T = EtoTauto_line_column= E;,which doesn't use
[[maybe_unused]].And that's good, because otherwise you wouldn't get a warning when
Tdoesn't have a non-trivial destructor.JohelEGP commentedon Oct 25, 2023
Here's an example where it's not possible, and actually made me discover a bug in
@basic_value: #453 (comment) (now #779).Don't generate a converting assignment operator if the user wrote one, …
JohelEGP commentedon Dec 23, 2023
In that particular case, now I can just omit the initialization by assigning
_to the member: https://cpp2.godbolt.org/z/eGja1q5Mf.Don't generate a converting assignment operator if the user wrote one, …