26

Why is the move constructor for Base mandatory in case of inheritance (class B) in the following code (both in gcc 7.2 and clang 4.0)? I would expect it not to be required with guaranteed copy elision in C++17, as in case of composition (class A).

struct Base {
    Base(Base&&) = delete;
    Base& operator=(Base&&) = delete;

    Base()
    {
    }
};

Base make_base()
{
    return Base{};
}

struct A {
    A() : b(make_base()) {} // <<<--- compiles fine

    Base b;
};

#ifdef FAIL
struct B : public Base {
    B() : Base(make_base()) {} // <<<--- "Base(Base&&) is deleted"
};
#endif

example

phuclv
  • 37,963
  • 15
  • 156
  • 475
Dev Null
  • 4,731
  • 1
  • 30
  • 46
  • 1
    What compiler are you using? And have you filed a bug with them on it? – Nicol Bolas Sep 06 '17 at 02:55
  • @NicolBolas gcc 7.2 / clang 4.0 both work the same way (see example - https://godbolt.org/g/w3jRuc). I haven't. – Dev Null Sep 06 '17 at 03:09
  • Yep, compiler bug. But what is that `#include ` doing there? – T.C. Sep 06 '17 at 04:24
  • @T.C., I removed `#include ` (it was a leftover from another example). – Dev Null Sep 06 '17 at 04:41
  • 2
    reported gcc bug 82113 – Dev Null Sep 06 '17 at 04:55
  • It seems like an opposite kind of bug to me. You've explicitly marked `Base` as non-movable but operation involving movement does not yield compile time error in case of composition. – user7860670 Sep 06 '17 at 06:08
  • 4
    @VTT, this is not a bug, but a c++17 feature called "guaranteed copy elision"; for example for `std::make_from_tuple` http://en.cppreference.com/w/cpp/utility/make_from_tuple `T` doesn't have to be movable. – Dev Null Sep 06 '17 at 06:22
  • @DevNull I'm aware of "guaranteed copy elision", but allowing a prohibited operation just because it will be omitted by "guaranteed optimization" seems like a serious flaw. – user7860670 Sep 06 '17 at 06:31
  • 1
    @VTT, could you please let me know what prohibits it? – Dev Null Sep 06 '17 at 06:38
  • @DevNull What do you mean by "why?". Any invocation of deleted function is prohibited. – user7860670 Sep 06 '17 at 06:47
  • 1
    @VTT guaranteed copy elision avoids invocations of deleted constructors, that's one of its features. – Dev Null Sep 06 '17 at 06:53
  • @DevNull Invocation of deleted function is present in source file therefore program is ill-formed. There is no exception of any kind for guaranteed copy elision to make program well-formed somehow. Also please note that "T doesn't have to be movable" is not the same as "movement of T is prohibited". – user7860670 Sep 06 '17 at 07:12
  • 1
    @VTT there's no invocation of deleted function in the source file compiled in C++17 mode, as per example http://coliru.stacked-crooked.com/a/e707b8ee1a871b0e. Guaranteed copy elision is specifically designed to allow returning by value for non-copyable, non-movable types. – Dev Null Sep 06 '17 at 07:19
  • 1
    Also please note that "T doesn't have to be movable for `std::make_from_tuple` to work" means that "`std::make_from_tuple` should work for non-movable types". – Dev Null Sep 06 '17 at 07:25
  • @DevNull You are wrong. Your example on coliru does not prove that source code contains no move constructor invocation just because no move constructor invocation happen in runtime (that is because it does not print corresponding line). `b(make_base())` is an invocation of move constructor that gets properly recognized and omitted due to copy elision. And I'm not arguing that "make_from_tuple should work for non-movable types". – user7860670 Sep 06 '17 at 07:28
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/153745/discussion-between-dev-null-and-vtt). – Dev Null Sep 06 '17 at 07:31
  • 6
    @VTT Initializing from a prvalue of the same type **does not invoke a constructor**. It's not that the move is elided. We shouldn't even do the lookup to find the move constructor here, there is no move. – Barry Sep 06 '17 at 15:48
  • 3
    This followup answer clarifies how guaranteed copy elision works in this particular case https://stackoverflow.com/questions/46071992/is-copy-move-elision-allowed-to-make-a-program-using-deleted-functions-well-form/46072220#46072220 – Dev Null Sep 07 '17 at 00:25
  • 2
    reported clang bug https://bugs.llvm.org/show_bug.cgi?id=34516 – Dev Null Sep 07 '17 at 06:25
  • The only constructor ever got called is the default one, that is Base(). The compiler aggressively do in-lining in the initializer list of struct B, causing B() collapsing to just Base(). No copy or move is ever made. Thus it does not matter if you deleted those two constructors. BTW, it must be a compiler bug. – John Z. Li Feb 13 '18 at 01:56

2 Answers2

16

According to Richard Smith:

This is a defect in the standard wording. Copy elision cannot be guaranteed when initializing a base class subobject, because base classes can have different layout than the corresponding complete object type.

Dev Null
  • 4,731
  • 1
  • 30
  • 46
  • Is this answer valid for a similar question https://stackoverflow.com/q/70898525/7325599 ? – Fedor Jan 29 '22 at 09:00
1

This is C++ Standard Core Language issue 2403, which is currently in open status.

The example given there is almost identical to yours:

  struct Noncopyable {
    Noncopyable();
    Noncopyable(const Noncopyable &) = delete;
  };

  Noncopyable make(int kind = 0);

  struct AsBase : Noncopyable {
    AsBase() : Noncopyable(make()) {} // #1
  };

  struct AsMember {
    Noncopyable nc;
    AsMember() : nc(make()) { }  // #2?
  };

And the comment says

All implementations treat #1 as an error, invoking the deleted copy constructor, while #2 is accepted. It's not clear from the current wording why they should be treated differently.

Actually all implementations treat #1 as an error is not quite true, since Visual Studio compiler performs copy elision here and accepts both this example and yours. Demo: https://gcc.godbolt.org/z/G61fKT55K

Fedor
  • 17,146
  • 13
  • 40
  • 131