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...)
It appears that Visual Studio doesn't implement P0136 yet. The correct C++17 behavior is a single copy, the correct 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.
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.