C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Should copying of the same type or its subtype be prohibited before constructing an object?

From: Jason McKesson <jmckesson_at_[hidden]>
Date: Sun, 19 May 2024 11:29:49 -0400
On Sun, May 19, 2024 at 10:34 AM David wang via Std-Proposals
<std-proposals_at_[hidden]> wrote:
>
> Jason McKesson Mon, 13 May 2024 11:33:39 -0400 via Std-Proposals
> <std-proposals_at_[hidden]> wrote:
>>
>>
>> But that isn't even a delegating constructor. At least, not in terms
>> of the C++ meaning of that term, as defined in [class.base.init]/6.
>>
>> If you mean it in the vernacular sense of "it calls some other
>> constructor", OK, but your initial post seems invalid. This isn't a
>> performance optimization; you're just asking for the thing you asked
>> for earlier in a different thread. So I don't know why you tried to
>> pass off the same request as a performance optimization.
>>
>> In any case, you need to address a very important point.
>>
>> What you want introduces an irregularity in the language. At the
>> present time, what this code means and does is clear and unambiguous.
>> Furthermore, it does the same thing it would do in any other case. If
>> you do this:
>>
>> ```
>> some_type t;
>> base u(t);
>> ```
>>
>> This will do the same thing, regardless of where it exists.
>>
>> What you want to do is change the language to make it irregular. Which
>> constructor will be called now will depend on the context of that
>> constructor call's location. In some places, it will call the template
>> constructor. In other places it won't.
>>
>> Irregularities are... bad, actually. Making code that looks the same
>> but behaves differently based on where that code is located is not a
>> good thing. It may sometimes be necessary or extremely useful. But you
>> need to actually make that argument. You need to explain why the cost
>> of irregularity is worth it.
>>
>> In particular, is this an idiom that people actually use? Is this a
>> common confluence of code that people see a lot? Because on the face
>> of it, the special case seems so rare that I don't buy that it needs
>> to be addressed. In order to encounter this problem, the following set
>> of things needs to happen:
>>
>> 1. A user writes a class intended to be used as a base class (note: it
>> is a code smell to use a class as a base class if that class isn't
>> intended for that purpose. It's not always wrong, but it should be
>> looked at as dubious).
>>
>> 2. The base class has a template constructor that takes exactly one
>> parameter by value (which suggests that this constructor has a meaning
>> that is very distinct from copying it. That is, it is not intended to
>> take itself as a parameter).
>>
>> 3. A user writes a class derived from this class. Seemingly unaware of
>> the base class's interface, the user attempts to invoke the base
>> class's copy constructor by passing itself as a parameter to the base
>> class constructor.
>>
>> Where exactly things went wrong here is debatable. I would argue that
>> #1 and #2 are inconsistent with each other. If you're making a class
>> that is intended to be a base class, you probably shouldn't create a
>> template constructor that would interfere with initializing that base
>> class. Note that types like `std::any`, despite not being intended to
>> be a base class, don't take their template parameter by value. But an
>> argument could be made that step 3 also is a bug, as the user should
>> have looked at the class's constructors before trying to use them.
>>
>> Regardless, the key question to me is this: how often do these
>> circumstances *actually* happen in the real world? Do they happen
>> often enough to warrant creating inconsistency in the behavior of the
>> language? Is there some important C++ idiom that's being blocked by
>> this?
>>
>> This has to happen often enough to pay the costs of making the
>> language inconsistent.
>
>
> I apologize for any confusion caused by my example.
>
> Calling a constructor for u(t) directly or indirectly, wherever it is, the argument is passed in a uniquely deterministic way which is not limited to calls to the delegated constructors. It applies to all constructor calls, everywhere. Just forget about template constructors and initial lists and focus on constructor calls and how to pass an argument.

You can't forget about those things because those circumstances are
the only reason why this matters at all.

Without template constructors, the only way this could even happen is
if someone writes a constructor that takes its own type by value, and
that constructor can be called with just an object of that type. This
would cause the constructor to conflict with the copy constructor.

But the language already recognizes that this is prima-facie wrong, so
it explicitly forbids it.

Which means that the only time this could actually be a problem is if
you're using a template, where you can accidentally bypass the thing
that would otherwise cause your code to break.

> But why would I recommend that copying of the same type or its subtypes should be prohibited before constructing an object? (If copying of the same type or its subtypes is prohibited before constructing an object, the argument will always be passed by reference if it is an object of the current class or its subclasses, and infinite loops will not occur in constructor calls.)

But that's not the same thing. If you prohibit copying "before
constructing an object", then what you're doing is making code that
tries to do that ill-formed. If you want code that says "copy this
object before constructing it" to instead *mean* "pass a reference",
that's a different request.

Saying that a construct is not allowed is a very different thing from
saying it should be allowed but invokes different behavior.

> Currently in C++, the behavior of function arguments is in most cases determined by the signature of the function. Function signatures bear too many responsibilities. While this relieves lots of the burdens on the caller, it also prevents the right (decide the behavior of function arguments) from the user and makes it obscure and difficult to understand. The behavior of argument is completely solidified in the function signature in most cases in C++!
>
> The function signature only needs to convey function's intents to the caller. It should be the user who has the right to decide how to pass arguments to the function, otherwise overloading will be very restrictive. For example, the move constructor in C++ communicates the intention of the move construction to the caller through the && symbol. And when constructing a new object, the caller must use std::move to call the corresponding constructor.

To the extent that this is true, this meaning works across all kinds
of functions. Constructors, regular functions, member functions,
rvalue-references mean the same thing and work in a consistent way.

What you want requires breaking consistency. You want "take a
parameter by value" to very occasionally mean "take a parameter by
reference".

> Similarly, We can define an intent communicator for the copy constructor, i.e. std::ref, to bear the responsibility of obtaining the object reference for the class name and & symbol in the function signature. We can define std::copy for the constructor that passes arguments by value, taking the responsibility of copying the object for the class name in the function signature.
>
> The function's writer only needs to decide what will happen to the argument which is told to the caller via function declaration. It is the caller who decide the behavior of function arguments according to the function declaration and the argument's usage environment. In fact, what will happen are determined by the compiler, which determines the behavior of the argument based on the syntax which is provided by the caller.
>
> Currently in C++, the syntax is provided by function declarations, but this approach does not provide a unified model of how the arguments are passed. The argument passed to function by value is not the real argument passed to function, it is the copy of the so-called argument. We are deceived and fooled by the model which masks the essence of things with convenient candy. The model is conceptually incomplete.
>
> The caller determines the diversity of overloading by providing various types of parameters. And the caller determines the powerful expressiveness of the assignment expression, not the producer of the rvalue. This caller's decision rights provides a unified model for all circumstance. This may seem extreme but handles various situations very uniformly. It is a huge subject and how to call the constructor is a good starting point.
>
> In the subject of constructor calls, the behavior of delegation constructor's argument can be determined by the writer at the time the constructor is formed, and the desired behavior is delegated to the compiler according to the corresponding syntax. On the contrary, users who call a constructor can also freely decide how to pass arguments to the constructor based on the declaration of the constructor and the calling environment. It is reasonable and there is no irregularity here.
>
> The syntax of C++ takes some simple ways to free users from boring work. But as new features (for example, rvalue references introduced in C++11) were introduced into C++, chaos emerged, such as inconsistent syntax for calling constructors and carelessness leading to infinite loops in calling constructors.
>
> We can say that this is the user's responsibility and that's the user's fault. We take this for granted simply because we are already familiar with how C++ works. From a newcomer’s perspective, isn’t this strange, difficult to understand, and irrational?

Is a newcomer likely to encounter this problem in their code? If not,
then what does it matter?

> Those simple coding ways that C++ syntax provides us is indeed very beneficial, in a practical, historically respectful way. But chaos is ultimately bad and unreasonable, making it more difficult to learn and understand.
>
> This irregularity or inconsistency in C++ syntax brings us great convenience, but the above-mentioned infinite loop, which can be resolved within the rule, does not bring us any benefits. The convenience provided by C++ syntax relieves the real responsibility of users, while the confusion caused by the syntax convenience is blamed on users.
>
> Due to reasons such as efficiency, habits, history, etc., there are always special situations in reality. The desire of how to pass arguments to function is indicated by the function declaration, but the behavior of the function argument must be the caller's responsibility (In actual language design and implementation, we often delegate it to the compiler).
>
> I went too far, and the title was too small to cover the topic at hand, but it was the best entry point to introduce it. The above statement seems to be extreme and wrong, but it has stronger descriptive power and a more convincing form of explanation, and may be able to handle various situations very uniformly. I hope the above statement can express my thoughts well, and I apologize for the obscurity and repetitiveness of the above narratives.

This whole 'philosophy of function calling' is nice and all, but
you're trying to use it to manifest changes that make the language
less regular. C++ is already filled to bursting with irregularities;
adding new ones because it better matches some philosophy of how
function calling should work is not a good idea.

I don't find arguments based on "conceptual incompleteness" to be
substantive next to the reality that what you want makes the language
less regular.

Received on 2024-05-19 15:30:04