Why isn't RVO applied to base class subobject initialization?
Asked Answered
R

2

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

Rorry answered 6/9, 2017 at 2:0 Comment(20)
What compiler are you using? And have you filed a bug with them on it?Thunell
@NicolBolas gcc 7.2 / clang 4.0 both work the same way (see example - godbolt.org/g/w3jRuc). I haven't.Rorry
Yep, compiler bug. But what is that #include <tuple> doing there?Arvad
@T.C., I removed #include <tuple> (it was a leftover from another example).Rorry
reported gcc bug 82113Rorry
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.Generatrix
@VTT, this is not a bug, but a c++17 feature called "guaranteed copy elision"; for example for std::make_from_tuple en.cppreference.com/w/cpp/utility/make_from_tuple T doesn't have to be movable.Rorry
@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.Generatrix
@VTT, could you please let me know what prohibits it?Rorry
@DevNull What do you mean by "why?". Any invocation of deleted function is prohibited.Generatrix
@VTT guaranteed copy elision avoids invocations of deleted constructors, that's one of its features.Rorry
@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".Generatrix
@VTT there's no invocation of deleted function in the source file compiled in C++17 mode, as per example coliru.stacked-crooked.com/a/e707b8ee1a871b0e. Guaranteed copy elision is specifically designed to allow returning by value for non-copyable, non-movable types.Rorry
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".Rorry
@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".Generatrix
Let us continue this discussion in chat.Rorry
@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.Gramineous
This followup answer clarifies how guaranteed copy elision works in this particular case #46072492Rorry
reported clang bug bugs.llvm.org/show_bug.cgi?id=34516Rorry
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.Eyelash
R
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.

Rorry answered 18/2, 2018 at 23:25 Comment(1)
S
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

Sparrow answered 30/1, 2022 at 7:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.