Deleted move constructor in base class does not stop derived class object to be returned from a function
Asked Answered
I

2

19

Given base class A and derived class B, A has deleted move constructor:

class A {
public: 
  A()  {}
  A(const A&) = default;
  A(A&&) = delete; 
};

class B : public A
{ 
};

In such case, the following function does not compile because of deleted move constructor:

A f() {
  A a;
  return a;
}

but the similar function for B does not report any error:

B g() {
  B b;
  return b;
}

Does it mean that move constructor in B is not deleted? I want to know what is the rule in the standard.

Ib answered 15/2, 2023 at 4:34 Comment(4)
IMO, the f case should compile as well, according to this paragraph (C++17). The relevant part: "If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue." GCC works for me, but Clang does not: godbolt.org/z/8EbcnfTfb.Fadge
But maybe I was wrong. This paragraph also seems to be relevant, which says that the program is actually ill-formed (as far as I understand it): timsong-cpp.github.io/cppwp/n4659/dcl.fct.def.delete#2.Fadge
@DanielLangr Yeah, gcc.gnu.org/bugzilla/show_bug.cgi?id=93106Gruber
A copyable-but-not-movable class is almost always a mistake, I'm not surprised they misbehave here.Aggregate
S
22

The move constructor in B is deleted, but does not participate in overload resolution. According to cppreference:

The implicitly-declared or defaulted move constructor for class T is defined as deleted if any of the following is true:

  • T has non-static data members that cannot be moved (have deleted, inaccessible, or ambiguous move constructors);
  • T has direct or virtual base class that cannot be moved (has deleted, inaccessible, or ambiguous move constructors);
  • T has direct or virtual base class or a non-static data member with a deleted or inaccessible destructor;
  • T is a union-like class and has a variant member with non-trivial move constructor.

A defaulted move constructor that is deleted is ignored by overload resolution (otherwise it would prevent copy-initialization from rvalue).

The second bullet point applies: B has a direct base class, A, with a deleted move constructor. So B's implicitly-declared move constructor is defined as deleted.

However, when the return statement is evaluating which constructor of B to use, the deleted move constructor is not considered but the valid copy constructor is.

Sienese answered 15/2, 2023 at 4:45 Comment(1)
Thanks for your answer! Then why the function A f() doesn't compile? Why can't it use the copy constructor of class A?Utility
F
13

The move constructor of B is deleted, as already answered by @Nathan Pierson.

The reason that you can return the local b from g is, as explained there, that the implicitly deleted move constructor of B is not participating in the overload resolution, thus the compiler selects the default copy constructor of B.

To prove the above take a look at the following code, adding a moveable only member into B:

class B : public A { 
  std::unique_ptr<int> ptr;
public: 
  B() {}
  // needs this to compile:
  //   B(B&& b): A(b), ptr(std::move(b.ptr)) {}  
};

B g() {
  B b;
  // now this would fail
  // B doesn't have a default move ctor
  // (it is implicitly deleted because of A)
  // and the default copy ctor is not valid
  return b; 
}
Floorwalker answered 15/2, 2023 at 4:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.