Date: Wed, 8 May 2024 09:34:21 -0400
On Wed, May 8, 2024 at 2:05 AM David wang via Std-Proposals <
std-proposals_at_[hidden]> wrote:
> On Tue, 7 May 2024 10:36:25 +0200 Sebastian Wittmeier wrote:
> >So in your case, you want to have a reference type deduced (by template
> argument deduction),
>
> >but currently [expr.type]/1 disagrees (
> https://timsong-cpp.github.io/cppwp/expr.type#1): >If an expression
> initially has the type ?reference to T? ([dcl.ref], [dcl.init.ref]), the
> type is >adjusted to T prior to any further analysis.
>
> >The copy constructor is something special (compared to other
> constructors).
> >Why do you want to handle it by a template
> >in the first place?
>
> Yes, I want to have a reference type deduced, but not through the
> template parameter deduction mechanism PR87332
> <https://github.com/llvm/llvm-project/pull/87332>. The template
> parameter deduction mechanism leads to T, not T&. "template <typename T>
> base(T x) {}" prevents the compiler from calling the default copy
> constructor.
>
No it doesn't.
Constructor templates are specifically excluded from being copy
constructors or move constructors. This exclusion causes problems
<https://gcc.gnu.org/bugzilla/show_bug.cgi?id=114817>, but it also means
that your `class Base` *totally does have* an implicitly defaulted copy
constructor, separate from the constructor template that you're trying not
to call.
So your code was:
struct Base {
int b;
template<class T> Base(T);
// here the compiler also automatically generates Base(const
Base&)=default
};
struct Derived : Base {
int d;
Derived(const Derived& rhs) : Base(rhs), d(rhs.d) {} // manually calls
Base::Base<Derived>(Derived)
};
Derived copy(const Derived& rhs) { return rhs; } // infinite recursion at
runtime
This code is buggy. But it also violates the Rule of Zero: you wrote code
*manually*, and you got it *wrong*, so naturally it's also going to do
the wrong thing. If you use the Rule of Zero, then what you have is:
struct Base {
int b;
template<class T> Base(T);
// here the compiler also automatically generates Base(const
Base&)=default
};
struct Derived : Base {
int d;
Derived(const Derived& rhs) = default; // automatically calls
Base::Base(const Base&)
};
Derived copy(const Derived& rhs) { return rhs; } // OK
So this leads us to a general rule of thumb:
- Always follow the Rule of Zero. Manually implementing special member
functions is tricky and should be avoided when possible.
And it leads us to a specific workaround in this case. If we really must
take control of `Derived`'s value semantics — if we can't delegate them to
some other "resource-management type" — then we must take the
responsibility for ensuring that the correct code is run for each thing
that we manually do.
struct Base {
int b;
template<class T> Base(T);
// here the compiler also automatically generates Base(const
Base&)=default
};
struct Derived : Base {
int d;
Derived(const Derived& rhs) : Base((const Base&)rhs), d(rhs.d) {} //
manually calls Base::Base(const Base&)
};
Derived copy(const Derived& rhs) { return rhs; } // OK
I notice in passing that both GCC and Clang generate worse codegen
<https://godbolt.org/z/1KPzzEWM1> for the manual approach than they do for
the Rule-of-Zero approach. So that's another reason to prefer the Rule of
Zero.
HTH,
–Arthur
std-proposals_at_[hidden]> wrote:
> On Tue, 7 May 2024 10:36:25 +0200 Sebastian Wittmeier wrote:
> >So in your case, you want to have a reference type deduced (by template
> argument deduction),
>
> >but currently [expr.type]/1 disagrees (
> https://timsong-cpp.github.io/cppwp/expr.type#1): >If an expression
> initially has the type ?reference to T? ([dcl.ref], [dcl.init.ref]), the
> type is >adjusted to T prior to any further analysis.
>
> >The copy constructor is something special (compared to other
> constructors).
> >Why do you want to handle it by a template
> >in the first place?
>
> Yes, I want to have a reference type deduced, but not through the
> template parameter deduction mechanism PR87332
> <https://github.com/llvm/llvm-project/pull/87332>. The template
> parameter deduction mechanism leads to T, not T&. "template <typename T>
> base(T x) {}" prevents the compiler from calling the default copy
> constructor.
>
No it doesn't.
Constructor templates are specifically excluded from being copy
constructors or move constructors. This exclusion causes problems
<https://gcc.gnu.org/bugzilla/show_bug.cgi?id=114817>, but it also means
that your `class Base` *totally does have* an implicitly defaulted copy
constructor, separate from the constructor template that you're trying not
to call.
So your code was:
struct Base {
int b;
template<class T> Base(T);
// here the compiler also automatically generates Base(const
Base&)=default
};
struct Derived : Base {
int d;
Derived(const Derived& rhs) : Base(rhs), d(rhs.d) {} // manually calls
Base::Base<Derived>(Derived)
};
Derived copy(const Derived& rhs) { return rhs; } // infinite recursion at
runtime
This code is buggy. But it also violates the Rule of Zero: you wrote code
*manually*, and you got it *wrong*, so naturally it's also going to do
the wrong thing. If you use the Rule of Zero, then what you have is:
struct Base {
int b;
template<class T> Base(T);
// here the compiler also automatically generates Base(const
Base&)=default
};
struct Derived : Base {
int d;
Derived(const Derived& rhs) = default; // automatically calls
Base::Base(const Base&)
};
Derived copy(const Derived& rhs) { return rhs; } // OK
So this leads us to a general rule of thumb:
- Always follow the Rule of Zero. Manually implementing special member
functions is tricky and should be avoided when possible.
And it leads us to a specific workaround in this case. If we really must
take control of `Derived`'s value semantics — if we can't delegate them to
some other "resource-management type" — then we must take the
responsibility for ensuring that the correct code is run for each thing
that we manually do.
struct Base {
int b;
template<class T> Base(T);
// here the compiler also automatically generates Base(const
Base&)=default
};
struct Derived : Base {
int d;
Derived(const Derived& rhs) : Base((const Base&)rhs), d(rhs.d) {} //
manually calls Base::Base(const Base&)
};
Derived copy(const Derived& rhs) { return rhs; } // OK
I notice in passing that both GCC and Clang generate worse codegen
<https://godbolt.org/z/1KPzzEWM1> for the manual approach than they do for
the Rule-of-Zero approach. So that's another reason to prefer the Rule of
Zero.
HTH,
–Arthur
Received on 2024-05-08 13:34:36