6

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...)

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055

2 Answers2

8

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • *the correct C++14 behavior was two copies*. Both GCC and Clang, make one copy of `Foo` even in C++14 mode: https://gcc.godbolt.org/z/oWveWPq3j – Fedor Feb 07 '22 at 11:32
  • 2
    @Fedor They used to: https://gcc.godbolt.org/z/6xaYcYd9c. It's just that they no longer implement the defect behavior because, well, defects? – Barry Feb 07 '22 at 14:29
  • 1
    @Fedor, C++14 mode does not mean "all the bugs and defects present in the C++14 standard", because that would make no sense. When a defect in the standard is fixed, it's usually intended that the fix applies to older standard modes too. Please don't report GCC bugs for "this defect was fixed and `-std=c++14` doesn't give me the defect back". That's not a bug. – Jonathan Wakely May 03 '22 at 17:11
3

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.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • http://eel.is/c++draft/ always links to the newest draft. If you want a specific C++ standard, follow the links from base website to find a historical version. The specific link for C++17: https://timsong-cpp.github.io/cppwp/n4659/class.inhctor.init#1 – Justin May 21 '19 at 15:06
  • @Justin Yeah tbh I just couldn't be bothered :P – Lightness Races in Orbit May 21 '19 at 15:08