Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should args to inherited constructors be copied when invoking the base ctor or not?

For the following program:

#include <iostream>

struct Foo
{
    Foo() { std::cout << "Foo()\n"; }
    Foo(const Foo&) { std::cout << "Foo(const Foo&)\n"; }
    ~Foo() { std::cout << "~Foo()\n"; }
};

struct A
{
    A(Foo) {}
};

struct B : A
{
    using A::A;
};

int main()
{
    Foo f;
    B b(f);
}

GCC gives:

$ g++ -std=c++17 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
Foo()
Foo(const Foo&)
~Foo()
~Foo()

VS 2017 (also in C++17 mode) gives:

Foo()
Foo(const Foo&)
Foo(const Foo&)
~Foo()
~Foo()
~Foo()

Who's right, and why?

(Let's also not forget that VS 2017 doesn't do mandated copy elision properly. So it could just be that the copy is "real" but GCC elides it per C++17 rules where VS doesn't...)

like image 351
Lightness Races in Orbit Avatar asked Jan 26 '23 07:01

Lightness Races in Orbit


2 Answers

It appears that Visual Studio doesn't implement P0136 yet. The correct C++17 behavior is a single copy, the original C++14 behavior was two copies.


The C++14 rules (N4140:[class.inhctor]) would interpret:

struct B : A
{
    using A::A;
};

as:

struct B : A
{
    B(Foo f) : A(f) { }
};

The introduced constructors are speciifed in p3, the mem-initializer equivalence in p8. Hence you get two copies of Foo: one into B's synthesized constructor and one into A's real constructor.


The C++17 rules, as a result of P0136, are very different (N4659:[class.inhtor.init]): there, we directly invoke A's constructor. It's not like we're adding a new constructor to B anymore - and it's not a mechanism that's otherwise expressible in the language. And because we're directly invoking A(Foo), that's just the one single copy instead of two.

like image 192
Barry Avatar answered Jan 28 '23 21:01

Barry


Elision notwithstanding, it looks to me like Visual Studio is wrong:

[C++17: class.inhctor.init]/1: When a constructor for type B is invoked to initialize an object of a different type D (that is, when the constructor was inherited ([namespace.udecl])), initialization proceeds as if a defaulted default constructor were used to initialize the D object and each base class subobject from which the constructor was inherited, except that the B subobject is initialized by the invocation of the inherited constructor. The complete initialization is considered to be a single function call; in particular, the initialization of the inherited constructor's parameters is sequenced before the initialization of any part of the D object.

like image 20
Lightness Races in Orbit Avatar answered Jan 28 '23 21:01

Lightness Races in Orbit