10

Consider following code:

struct S
{
    S() = default;
    S(S const&) = delete;
    // S(S&&) = delete;  // <--- uncomment for a mind-blowing effect:
                         // MSVC starts compiling EVERY case O_O
};

S foo() { return {}; }

struct X : S
{
//    X() : S(foo()) {}   // <----- all compilers fail here
};

struct Y
{
    S s;
    Y() : s(foo()) {}   // <----- only MSVC fails here
};

struct Z
{
    S s = {};           // ... and yet this is fine with every compiler
    Z() {}
};

//S s1(foo());      // <-- only MSVC fails here
//S s2 = foo();     // <-- only MSVC fails here

Questions:

  • It looks like there is no way to initialize non-copyable base class with a prvalue -- is this correct? Looks like a deficiency in standard (or all compilers I tried are non-compliant)

  • MSVC can't initialize member variable -- does it mean it is non-compliant? Is there a way to workaround this?

  • why adding S(S&&) = delete; causes MSVC to compile every case?

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
C.M.
  • 3,071
  • 1
  • 14
  • 33
  • Is `S` moveable? If you add `S(S&&) = default;` both of those bottom two cases should compile. – Cory Kramer Jul 15 '20 at 18:24
  • @CoryKramer Well, it obviously would work if S is moveable. :) But you pushed me to another discovery -- explicitly deleting mctor causes MSVC to compile every case. O_O – C.M. Jul 15 '20 at 18:31
  • What version of MSVC are you using? What language flag do you have specified? – NathanOliver Jul 15 '20 at 18:32
  • @NathanOliver check provided godbolt link (latest available, `c++latest`) – C.M. Jul 15 '20 at 18:33
  • Have you tried whether `X` calls `S` copy constructor, when it exists? – n314159 Jul 15 '20 at 18:49
  • @n314159 No, but I expect it to be elided. For GCC/clang it is easy to check in [wandbox.org](http://wandbox.org). – C.M. Jul 15 '20 at 19:12
  • @NicolBolas, I do not appreciate you editing bits of humor out of my post. Don't care how you'd justify this censorship. – C.M. Jul 15 '20 at 20:27
  • @C.M.: I have no actual power to stop you from putting it back, you know. I removed it because it was off-topic, since the image macro in question has to do with initialization forms/mechanism, but your problem is due to guaranteed elision being implemented incorrectly on various compilers. – Nicol Bolas Jul 15 '20 at 21:50
  • @NicolBolas I know I can put it back. But this may lead to editing war and I have no time nor desire for it. Just wanted to poke you back for (perceived) censorship. :) – C.M. Jul 15 '20 at 22:10
  • Partially, dup of [Why isn't RVO applied to base class subobject initialization?](https://stackoverflow.com/questions/46065704/why-isnt-rvo-applied-to-base-class-subobject-initialization) – Language Lawyer Jul 16 '20 at 13:01

3 Answers3

2

So, I think I found the relevant parts of the standard and I think the compilers are in error regarding to X. (All links are to a standard draft so very maybe it was different in C++17, I will check that later. But gcc10 and clang10 also fail with -std=c++20, so that is not that important).

Regarding the initialization of base classes (emphasis mine): class.base.init/7

The expression-list or braced-init-list in a mem-initializer is used to initialize the designated subobject (or, in the case of a delegating constructor, the complete class object) according to the initialization rules of [dcl.init] for direct-initialization.

I think this tells us, that X() : S(foo()) {} should not be different from S s = foo(), but let's look at dcl.init/17.6.1

If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [Example: T x = T(T(T())); calls the T default constructor to initialize x. — end example]

This implies to me, that X() : S(foo()) {} should call the default constructor. I also tested (to be completely in line with the example) X() : S(S()) {} and this also fails on clang and g++. So it seems to me that the compilers have a defect.

n314159
  • 4,990
  • 1
  • 5
  • 20
  • 1
    Nit: `S s = foo()` is not direct initialization. It's copy initialization, due to the `=`. Doesn't matter though; actual direct initialization `S s(foo())` also works. – HTNW Jul 15 '20 at 20:19
  • 2
    This is a defect in the standard. So-called "guaranteed elision" cannot work on potentially-overlapping subobjects. – T.C. Jul 15 '20 at 23:13
  • @T.C. Any idea if this defect is being addressed? Are we going to have guaranteed elision in this case in some future C++ version? – C.M. Jul 16 '20 at 17:56
0

No, it's not allowed, but since c++17, there are some new features, and one of them, functions no more copy object.

Functions returning prvalues no longer copy objects (mandatory copy elision), and there is a new prvalue-to-glvalue conversion called temporary materialization conversion. This change means that copy elision is now guaranteed, and even applies to types that are not copyable or movable. This allows you to define functions that return such types.

Guaranteed copy elision C++17


The following function, never return S() object and return, std::initialization_list or {}, instead, as S s ={}, is a valid conversion, so based on copy elision optimization, it's not going to return a copy and roughly speaking - directly return std::initialization_list itself. Note temporary materialization conversion.

S foo() { return {}; }

The following not going to work,

S foo() { S s = {}; return s; }

So that, the line Y() : s(foo()) {}, roughly speaking - now could be interpreted as implicit type conversion,

S s = {}
4.Pi.n
  • 1,151
  • 6
  • 15
0

It looks like there is no way to initialize non-copyable base class with a prvalue -- is this correct? Looks like a deficiency in standard (or all compilers I tried are non-compliant)

The standard says that it should work. The standard is wrong.

A base class subobject (more generally, a potentially-overlapping subobject) may have a different layout from a complete object of the same type, or may have its padding reused by other objects. It is therefore impossible to elide a copy or move from a function returning a prvalue, since the function has no idea that it is not initializing a complete object.

The rest are MSVC bugs.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Can you expand a bit on "potentially-overlapping subobject", pls? What is it and why it may have a different layout? I find it hard to believe, simply because when I pass `T*` to another function -- that function always expects same layout regardless if pointer points to a sub-object or a complete object. Hmm... could be smth to do with vtables? – C.M. Jul 15 '20 at 23:34
  • Virtual bases. Padding reuse is by far more common though - the initialization of a complete object is free to trample over padding bytes; if the object is actually a base class subobject, those bytes may be occupied by some other object. – T.C. Jul 15 '20 at 23:39
  • The standard isn't wrong; all of those things are up to the compiler implementation to *enforce*. Layout, overlapping, all of those are under the compiler's control. And if the compiler wants to have padding reuse in a way where guaranteed elision would overwrite some other subobject that had already been initialized... then the compiler *cannot* overlap with that object. Overlapping is not required by the standard; it's merely an optimization, one compilers have to avoid if it would break stuff. – Nicol Bolas Jul 16 '20 at 03:27
  • Note that if the object undergoing guaranteed elision gets initialized *before* initializing any objects that overlap into its padding, this is fine. Thus, EBO overlapping works OK, since the order of initialization is clear and no overlapping of empty classes with non-empty types can cause out-of-order initialization problems. Also, fully empty types don't have to care, since they don't actually do anything to their "storage". – Nicol Bolas Jul 16 '20 at 03:28
  • 1
    @NicolBolas Considering that the paper claims to be formalizing existing practice about copy elision, "wrong" is a good description, and "unimplementable without breaking current ABIs" would be equally as good. Fully empty types _do_ have to care, because when value-initialization performs zero-initialization, it is required to zero out the padding, and that's observable by inspecting the object representation; that padding might overlap with an already-initialized base class object; `[[no_unique_address]]` members are just as problematic. (This is being tracked as core issue 2403.) – T.C. Jul 16 '20 at 04:04
  • @T.C.: "*that's observable by inspecting the object representation*" Zero-sized subobjects have no object representation, as they are zero-sized. This is why the standard has specific wording about trivially copying from/to base classes. – Nicol Bolas Jul 16 '20 at 05:49
  • @NicolBolas The point is that there's no mechanism in existing ABIs to allow a `T f() { return T();}` to zero out the padding when `f()` is used to initialize a complete object but not when it is used to initialize a zero-sized subobject and still get guaranteed elision in both cases. – T.C. Jul 16 '20 at 13:16
  • @T.C.: And my point is that initializing padding only matters if you initialize the objects in the wrong order. So long as the layout of objects does not put an object that gets initialized earlier into the storage of an object that gets initialized later, if the two are not both empty, then everything is fine. – Nicol Bolas Jul 16 '20 at 13:21
  • I agree with @NicolBolas here, this whole "can't initialize subobject via RVO because overlaps" is either a red herring or a defect in a standard that can be fixed (probably by updating rules related to dealing with padding during construction). From pure implementation side (and according to my, admittedly limited, understanding) there is no good reason why another function can't construct a subobject at given address. Weird cases (like virtual inheritance) can be addressed too. – C.M. Jul 16 '20 at 17:54
  • @NicolBolas No, consider `struct X : A, B { X(); };` where `A` is not empty and `B` is empty and initialized from a function call to `B f();`. Current implementations layout `B` over `A`, and `f()` cannot be allowed to clobber `A`'s memory. Any changes in this area is a major ABI break that's unlikely to happen. – T.C. Jul 16 '20 at 19:26
  • @T.C.: If `B` is empty, then how could `B` (legally) clobber any of its storage? Its constructor can't `memcpy` into itself, because trivially copyable rules explicitly forbid `memcpy` actions on base classes. And how could you tell if it clobbered anything? Again, the trivially copyable rules forbid reading from base classes. – Nicol Bolas Jul 16 '20 at 21:01
  • @NicolBolas `B f() { return B(); } B b = f();` is guaranteed to zero out the padding in `b`, and since `b` is a complete object you can look at it. Yet `X() : A(1), B(f()) {}` cannot be allowed to clobber, because that breaks `A` . – T.C. Jul 16 '20 at 21:07
  • @T.C. Actually, there is a way to do that. From `X() : A(1), B(f()) {}`, the compiler can plainly see that `B` is empty and that `f` will initialize it. So initialize something else. Pass `f` a value on the stack instead of `B`'s actual address. That way, it will clobber nothing, and since the value of `B` is irrelevant, it doesn't really matter. This is simply how it would be implemented, not how the standard would define it. – Nicol Bolas Jul 16 '20 at 21:45
  • @NicolBolas This is observable; the constructor of `B` (or a constructor called by it) can take `this` and stash it for later comparison. – T.C. Jul 16 '20 at 21:53