Which compiler is doing the right thing here?

92 views
Skip to first unread message

Belloc

unread,
Sep 4, 2016, 10:30:57 AM9/4/16
to ISO C++ Standard - Discussion
I'm experimenting with this snippet I found in DR 670

struct A {};

struct B: A {
    B
(int);
    B
(B&);
    B
(A);
};

void foo(B);
void bar() {
    foo
(0);
}

After a few trials I arrived at the following code:

#include <iostream>

struct A {
    A
() { std::cout << "A{}" << '\n'; }
    A
(const A&) { std::cout << "A(const A&)" << '\n'; }
};

struct B: A {
       B
(int) { std::cout << "B(int)" << '\n'; }
       B
(B&) { std::cout << "B(B&)" << '\n'; }
       B
(const B& b) : A(b) { std::cout << "B(const B&)" << '\n'; }
       B
(A) { std::cout << "B(A)" << '\n'; }
};

void foo(B) { std::cout << "foo(B)" << '\n'; }
 
int main() { foo(0); }


Both GCC and clang compile the code (see live example) printing:

A{}
B
(int)
A
(const A&)
B
(const B&)
foo
(B)

which seems to be correct to me. VS2015 also compiles the code, but prints

A{}
B
(int)
foo
(B)

because it always does copy elision, as far as I can understand.

Therefore, all three compilers seem to be in agreement here. Now, let's comment out B's copy constructor.

#include <iostream>

struct A {
    A
() { std::cout << "A{}" << '\n'; }
    A
(const A&) { std::cout << "A(const A&)" << '\n'; }
};

struct B: A {
    B
(int) { std::cout << "B(int)" << '\n'; }
    B
(B&) { std::cout << "B(B&)" << '\n'; }
//  B(const B& b) : A(b) { std::cout << "B(const B&)" << '\n'; }
    B
(A) { std::cout << "B(A)" << '\n'; }
};

void foo(B) { std::cout << "foo(B)" << '\n'; }

int main() { foo(0); }

Now GCC prints (live example)

A{}
B
(int)
A
(const A&)
A
{}
B
(A)
foo
(B)

and I have no idea where these: A{} and B(A) came from.

clang fails to compile the code and VS2015 prints

A{}
B
(int)
foo
(B)

Which compiler is doing the right thing here and why?

I'm particularly interested in knowing the quotes from the Standard supporting the answer. Thanks.

Edward Catmur

unread,
Sep 5, 2016, 6:59:09 AM9/5/16
to ISO C++ Standard - Discussion
On Sunday, 4 September 2016 15:30:57 UTC+1, Belloc wrote:
I'm experimenting with this snippet I found in DR 670
<snip> 
Now, let's comment out B's copy constructor.

#include <iostream>

struct A {
    A
() { std::cout << "A{}" << '\n'; }
    A
(const A&) { std::cout << "A(const A&)" << '\n'; }
};

struct B: A {
    B
(int) { std::cout << "B(int)" << '\n'; }
    B
(B&) { std::cout << "B(B&)" << '\n'; }
//  B(const B& b) : A(b) { std::cout << "B(const B&)" << '\n'; }
    B
(A) { std::cout << "B(A)" << '\n'; }
};

void foo(B) { std::cout << "foo(B)" << '\n'; }

int main() { foo(0); }

Now GCC prints (live example)

A{}
B
(int)
A
(const A&)
A
{}
B
(A)
foo
(B)

and I have no idea where these: A{} and B(A) came from.

First, gcc copy-initializes ([expr.call]/4, [dcl.init]/1, [dcl.init]/15, [dcl.init]/17]) a prvalue B from 0, printing A{} and B(int). It then direct-initializes ([dcl.init]/17) the parameter B from the prvalue B, enumerating all the constructors ([over.match.ctor]) with argument list prvalue B; the copy constructor is not viable (B& cannot bind to a prvalue: [over.match]/2, [over.match.viable]/3, [over.best.ics]/5, [over.ics.ref]/3), so as discussed in CWG 670 it selects B(A). The parameter A is direct-initialized from the prvalue B converted to A via derived-to-base conversion, selecting A(A const&) where the reference parameter is bound to the A subobject of an xvalue B materialized ([conv.rval]) from the prvalue B ([dcl.init.ref]/5), printing A(const A&); then B(A) prints A{} and B(A). We can now finally enter foo(B) and print foo(B).

clang fails to compile the code

This is incorrect per the discussion in CWG 670, at least after CWG 391. Jason Merrril's argument is that prior to CWG 391 we could not initialize the parameter of A(A const&) from the prvalue B because [dcl.init.ref]/5 required a copy constructor of the prvalue B to be callable, but I'm not sure I agree; as far as I can see B(A) would have done just as well here. In any case I don't believe that clang is behaving as a pre-CWG 391 compiler here; note that it accepts the following when in C++11 mode, which a pre-CWG 391 compiler would not (in C++03 mode its diagnostic for the following is "C++98 requires an accessible copy constructor for class 'A' when binding a reference to a temporary; was private [-Werror,-Wbind-to-temporary-copy]"):

struct A { A(A const&) = delete; };
A f
();
A
const& a = f();

I think it is more likely that clang is applying additional (unwarranted) restriction to [over.best.ics]/4, as Steve Adamczyk mentions on CWG 670, and thus refusing to consider B(A) for the direct-initialization of the parameter B.

and VS2015 prints

A{}
B
(int)
foo
(B)

It appears that VS is failing to observe the final sentence of [dcl.init]/17.6.3. Note that this is not a copy elision permitted by [class.copy]/31.

Which compiler is doing the right thing here and why?

gcc, as above.

I'm particularly interested in knowing the quotes from the Standard supporting the answer. Thanks.
 
I'll let someone else dig out the exact sentences :)

Belloc

unread,
Sep 5, 2016, 8:56:10 AM9/5/16
to ISO C++ Standard - Discussion


On Monday, September 5, 2016 at 7:59:09 AM UTC-3, Edward Catmur wrote: (emphasis is mine)

First, gcc copy-initializes ([expr.call]/4, [dcl.init]/1, [dcl.init]/15, [dcl.init]/17]) a prvalue B from 0, printing A{} and B(int). It then direct-initializes ([dcl.init]/17) the parameter B from the prvalue B, enumerating all the constructors ([over.match.ctor]) with argument list prvalue B; the copy constructor is not viable (B& cannot bind to a prvalue: [over.match]/2, [over.match.viable]/3, [over.best.ics]/5, [over.ics.ref]/3), so as discussed in CWG 670 it selects B(A). The parameter A is direct-initialized from the prvalue B converted to A via derived-to-base conversion, selecting A(A const&) where the reference parameter is bound to the A subobject of an xvalue B materialized ([conv.rval]) from the prvalue B ([dcl.init.ref]/5), printing A(const A&); then B(A) prints A{} and B(A). We can now finally enter foo(B) and print foo(B).

That doesn't seem to be correct. The copy constructor B(const B&) was viable in the first example and it was selected as the best match. How come the default copy constructor is not viable and is not the best match in the second example. See [class.copy]/7:

"If the class definition does not explicitly declare a copy constructor, a non-explicit one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted ([dcl.fct.def]). The latter case is deprecated if the class has a user-declared copy assignment operator or a user-declared destructor."

Edward Catmur

unread,
Sep 5, 2016, 9:04:25 AM9/5/16
to std-dis...@isocpp.org
B(B&) is a copy constructor ([class.copy]/2), so it suppresses generation of the implicit copy constructor. Note that even an implicitly-declared copy constructor can have form X(X&) if necessary ([class.copy]/8).

Belloc

unread,
Sep 5, 2016, 9:09:52 AM9/5/16
to ISO C++ Standard - Discussion
I take back what I said earlier, as [class.copy]/7 is valid as long as the class definition does not explicitly declare a copy constructor, which is not the case in the second example, as a copy constructor B(B&) was declated.

T. C.

unread,
Sep 5, 2016, 6:34:42 PM9/5/16
to ISO C++ Standard - Discussion
If we are speaking in terms of temporary materialization (i.e., post-P0135), then the new bullet in [dcl.init]/17 takes precedence and elides the second mess entirely.

Edward Catmur

unread,
Sep 6, 2016, 8:38:25 AM9/6/16
to std-dis...@isocpp.org
Ah, thanks. So gcc is correct only as a pre-P0135 compiler.

And MSVC is correct as either a pre-P0135 compiler or a post-P0135 compiler; in the latter case, because of the new bullet in [dcl.init]/17, in the former case via optional copy elision language deleted by P0135 [class.copy]/31.3:

when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same type (ignoring cv-qualification), the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

That makes a lot more sense now, thanks!
Reply all
Reply to author
Forward
0 new messages