Unexpected const reference behavior
Asked Answered
G

3

8
#include <iostream>

class A { 
  public:  
    A(){ cerr << "A Constructor" << endl; }  
    ~A(){ cerr << "A Destructor" << endl; }  
    A(const A &o){ cerr << "A Copy" << endl; } 
    A& operator=(const A &o){ cerr << "A Assignment" << endl; return *this; }
};


class B : public A { 
  public:  
    B() : A() { cerr << "B Constructor" << endl; }  
    ~B(){ cerr << "B Destructor" << endl; }
  private:
    B(const B &o) : A() { cerr << "B Copy" << endl; } 
    B& operator=(const B &o){ cerr << "B Assignment" << endl; return *this; }
};

int main() {  
  A a;  
  const A &b = B();  
  return 0; 
}

In GCC 4.2, I get this message:

In function 'int main()':
Line 16: error: 'B::B(const B&)' is private
compilation terminated due to -Wfatal-errors.

If I remove the "private" from B, I get the output I expect:

A Constructor
A Constructor
B Constructor
B Destructor
A Destructor
A Destructor

My question is: why does making a method which isn't called private change whether this code compiles? Is this standard-mandated? Is there a workaround?

Gisborne answered 14/7, 2010 at 18:17 Comment(10)
I don't see why this shouldn't compile. FWIW, Comeau agrees with me.Surat
@sbi: Curiously, Comeau rejects the code with C++0x extensions disabled, but accepts the code with C++0x extensions enabled.Bandmaster
No error with g++ 4.4.2, FWIW.Madcap
gcc 4.5 accepts the code with and without C++0x extension.Pickerel
@James McNellis: In C++03, when initializing a const reference from an rvalue the implementation is allowed to construct a temporary from the rvalue and bind the reference to that temporary (it's allowed to do this recursively!) or bind directly to the rvalue. Either way the copy constructor is required to be accessible whether used or not. C++0x has revisited this and if the rvalue is reference compatible the reference must be bound directly to the rvalue; a temporary is no longer allowed and so the restriction on having an available copy-constructor has also been lifted.Sivas
It's 8.5.3/5 in both C++03 and C++0x FCD.Sivas
@Charles: Yep; I didn't know that when I posted that comment; after doing some research, I just posted that in an answer with relevant quotes (well, I think they are the relevant quotes).Bandmaster
@James McNellis: Ah sorry, I hadn't see that yet.Sivas
@pmr: gcc 4.5 accepts the code without the C++0x extension? That turns out to be a bug, then.Gisborne
@James McNellis, Charles Bailey: Thanks, that answers my question.Gisborne
B
4

The important verbiage in the current standard (C++03) seems to be in §8.5.3, which explains how references are initialized (In these quotes, T1 is the type of the reference being initialized and T2 is the type of the initializer expression).

If the initializer expression is an rvalue, with T2 a class type, and "cv1 T1" is reference-compatible with "cv2 T2," the reference is bound in one of the following ways (the choice is implementation-defined):

-- The reference is bound to the object represented by the rvalue (see 3.10) or to a sub-object within that object.

-- A temporary of type "cv1 T2" [sic] is created, and a constructor is called to copy the entire rvalue object into the temporary. The reference is bound to the temporary or to a sub-object within the temporary.

The constructor that would be used to make the copy shall be callable whether or not the copy is actually done.

So, even if the implementation binds the reference directly to the temporary object, the copy constructor must be accessible.

Note that this is changed in C++0x, per the resolution of CWG defect 391. The new language reads (N3092 §8.5.3):

Otherwise, if T2 is a class type and

-- the initializer expression is an rvalue and "cv1 T1" is reference-compatible with "cv2 T2,"

-- T1 is not reference-related to T2 and the initializer expression can be implicitly converted to an rvalue of type "cv3 T3" (this conversion is selected by enumerating the applicable conversion functions (13.3.1.6) and choosing the best one through overload resolution (13.3)),

then the reference is bound to the initializer expression rvalue in the first case and to the object that is the result of the conversion in the second case (or, in either case, to the appropriate base class subobject of the object).

The first case applies and the reference is "bound directly" to the initializer expression.

Bandmaster answered 14/7, 2010 at 19:3 Comment(2)
The resolution note / standard talks about subobjects. Obviously subobjects have polymorphism going on, which was confusing me for a little bit. Out of curiousity, does the standard talk about narrowing somewhere? I can't find anything in a 2005 working copy I'm using, but I think I just don't know what to search for. I was looking under 'standard conversion' and the section on copy constructors.Gisborne
@Zachary: One "feature" of the C++ standard is that topics are often spread across six different sections, making it quite difficult to find information in it if you aren't familiar with where things might be. In this case, the subclause on Declarator Initializers (§8.5) is important because you are initializing the reference. §12.2 on Temporary Objects is important because you have a temporary class-type object. Often, the important part is a single, short sentence in the middle of a paragraph. Sometimes I find that it helps to search for a key word (e.g., "reference").Bandmaster
T
3

So what you're using is 'copy-initialization':

8.5/11 Initializers

The form of initialization (using parentheses or =) is generally insignificant, but does matter when the entity being initialized has a class type; see below. ...

The initialization that occurs in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and brace-enclosed initializer lists (8.5.1) is called copy-initialization and is equivalent to the form

T x = a;

The initialization that occurs in new expressions (5.3.4), static_cast expressions (5.2.9), functional notation type conversions (5.2.3), and base and member initializers (12.6.2) is called direct-initialization and is equivalent to the form

T x(a);

In 13.3.1.3 "Initialization by constructor", the overloads for the constructor chosen are:

When objects of class type are direct-initialized (8.5), or copy-initialized from an expression of the same or a derived class type (8.5), overload resolution selects the constructor. For direct-initialization, the candidate functions are all the constructors of the class of the object being initialized. For copy initialization, the candidate functions are all the converting constructors (12.3.1) of that class.

So, for copy-initialization, the copy constructor must be available. However, the compiler is permitted to 'optimize away' the copy:

12.2/1 Temporary objects

Even when the creation of the temporary object is avoided (12.8), all the semantic restrictions must be respected as if the temporary object was created. [Example: even if the copy constructor is not called, all the semantic restrictions, such as accessibility (clause 11), shall be satisfied. ]

You can get the effect you want by avoiding copy-initialization and using direct-initialization:

 const A &b(B());  

Note:

Since newer versions of GCC apparently have a different behavior, I thought I'd post this note, which might address the difference (with both behaviors still standards conforming):

8.5.3/5 References says:

If the initializer expression is an rvalue, with T2 a class type, and “cv1 T1” is reference-compatible with “cv2 T2,” the reference is bound in one of the following ways (the choice is implementation-defined):

  • The reference is bound to the object represented by the rvalue (see 3.10) or to a sub-object within that object.

  • A temporary of type “cv1 T2” [sic] is created, and a constructor is called to copy the entire rvalue object into the temporary. The reference is bound to the temporary or to a sub-object within the temporary.

The constructor that would be used to make the copy shall be callable whether or not the copy is actually done.

I originally read the last sentence ("the constructor that would be used...") to apply to both options, but maybe it should be read as only applying to the seconds option - or at least maybe that's how the GCC maintainers are reading it.

I'm not sure if this is what's going on between the differing behavior of GCC versions (comments welcome). We're definitely reaching the limits of my language-lawyering skills...

Tatouay answered 14/7, 2010 at 18:48 Comment(7)
The entity being initialized is not an object, though, it is a reference.Bandmaster
This was my first idea, too, but it must be a compiler bug because otherwise later versions of gcc would hardly change the behavior.Noni
@Michael Burr: I don't understand why direct vs. copy initialization matters for references. 8.5/11 says "see below;" the first bullet of 8.5/14 (which is the "below") says "If the destination type is a reference type, see 8.5.3," and 8.5.3 makes no distinction between direct and copy initialization.Bandmaster
8.5.3/5 discusses this and does indicate that for a reference that's bound to a temporary "The constructor that would be used to make the copy shall be callable whether or not the copy is actually done"Tatouay
@Michael Burr: Right. However, if we are initializing an object, both direct and copy initialization require an accessible copy constructor. Why would direct initialization of a reference not require an accessible copy constructor?Bandmaster
@James and Philipp: I've added a note that I think covers both comments (maybe).Tatouay
@Michael: I too read the last sentence as applying to both options, but I agree that it's a bit ambiguous. Now I really have no idea. :-) Ahh, the joy of language standards.Bandmaster
N
1

I think it is indeed a compiler bug, gcc seems to think that is is copy initialization. Use direct initialization instead:

const A& b(B());

The copy-constructor call in copy initialization is always optimized away (an instance of copy elision), and then doesn't have to be available.

Noni answered 14/7, 2010 at 18:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.