C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Pass derived object by reference to single parameter delegated templated constructors

From: Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
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

Received on 2024-05-08 13:34:36