On Wed, Apr 30, 2008 at 1:51 PM, Scott Thompson <email@hidden> wrote:
> I've run into an interesting problem while compiling some legacy code with
> gcc (code that is to be made cross-platform). The solution is perplexing me
> so I thought I would ask here to see if anyone had ideas. I've boiled the
> problematic code down to this small sample:
>
>
> class Owned
> {
> public:
> Owned() {};
> };
>
> class Inited
> {
> public:
> Inited();
> Inited(Owned *owned);
> Inited &operator=(Owned* itemToAssign);
>
> // Problematic Delcaration
> Inited(Inited &itemToCopy);
> };
>
> Owned *makeOwned()
> {
> return new Owned;
> }
>
> int main (int argc, char * const argv[]) {
> /* ** Compile error here ** */
> Inited myInited = makeOwned();
> }
>
> The compile error is:
>
> main.cpp:25: error: no matching function for call to
> 'Inited::Inited(Inited)'
> main.cpp:15: note: candidates are: Inited::Inited(Inited&)
> main.cpp:11: note: Inited::Inited(Owned*)
> main.cpp:25: error: initializing temporary from result of
> 'Inited::Inited(Owned*)'
>
> So it would appear that GCC is trying to create a temporary and then
> initialize the "myInited" from that temporary. I gather that would be
> roughly equivalent to:
>
> const Inited t1(makeOwned()); Inited myInited(t1);
>
> Because the temporary is const, it can't use the copy constructor with the
> non-const parameter. But, apparently, the presence of this constructor is
> what causes it to create the temporary in the first place. If I remove this
> copy constructor then everything compiles without trouble.
>
> I can also fix the problem by declaring:
>
> Inited(const Inited &itemToCopy); /* note the addition of a const */
>
> While adding that that makes sense for this toy example, it doesn't work in
> the original code where "Inited" is a "smart pointer" object. The
> "itemToCopy" is expected to relinquish control of the pointer it contains to
> the newly initialized object and is, therefore, not const in this context.
>
> I could also change the line of code where the error is reported to:
>
> Inited myInited(makeOwned());
>
> Again in the toy code this is not really a problem, but the original code
> base uses the version with the equals sign "all over the place". That code
> passes through several other C++ compilers without issue, but it won't
> compile through XCode.
To which Clark Cox replied:
Unfortunately, the other compilers are wrong as far as the C++
standard is concerned. The C++ standard requires that when
initializing with '=', as you do above, a (const&) copy constructor is
required. When you remove your 'Inited(Inited &itemToCopy)'
constructor, the compiler provides an implicit 'Inited(const Inited
&itemToCopy)'.
What is it about your smart pointer class that prevents you from
providing a proper copy constructor?
I realize that this is a "blast from the past" having originally been posted in April. I've had the occasion to look into this problem a little deeper and thought I would share. For those of you that aren't into C++ minutiae, please feel free to skip this post.
To answer your question. The thing that prevents me from providing a "proper" copy constructor (i.e. Inited(const &Inited itemToCopy)) to my smart pointer class is the same thing that prevents std::auto_ptr<T> from having a "proper" copy constructor. Namely this:
void function_of_evil(std::auto_ptr<int> numberPtr)
{
}
/* const std::auto_ptr must remain const! You cannot take his pointer away */
const std::auto_ptr<int> myConstPtr(new int(3));
/* This call tries to take away his pointer. Big meanie. */
function_of_evil(myConstPtr);
In the world of STL, you cannot "take the pointer away" from a const std::auto_ptr<T>. Doing so would violate the const-ness of the auto-pointer object. To prevent this they declare the copy constructor as std::auto_ptr<T>(std::auto_ptr<T> &rhs) (note the "improper" copy constructor omitting the const just as in Inited above). This means that the "proper" copy constructor is overtly suppressed.
However this leads to a conflict. This conflict is the same one shared by Inited which has both
Inited(Inited &rhs) and
Inited(Owned *rhs)
In the case of object initialization above (Inited myInited = makeOwned()), the compiler would normally create a temporary and use direct initialization from the temporary. The statement would, in effect become something like:
Inited myInited((const Inited &) const Inited temporary(makeOwned()));
This option is closed, however, as it would require a copy constructor that takes a (const Inited &). As you said, we've directly suppressed that by declaring the non-const copy constructor.
However, the compiler is allowed to make an optimization where, when using "=" to initialize a class type, t can directly construct the object specified by the initializer into the source object. As a result of this optimization, the compiler thinks it has two choices. The first is to direct initialization through a non-const reference:
Inited myInited((Inited &) Inited(makeOwned()))
(understanding that, because of the optimization, the temporary object implied by this statement would never be created)
And the second is direct initialization through the "raw pointer" copy constructor given in the code:
Inited myInited(makeOwned())
gcc sees these to options as being distinct and ranked equally. Semantically, you and I know it doesn't really amount to a hill of beans which of the two is chosen, they both do the exact same thing. Unfortunately the compiler doesn't know that the content of our two initializers are the same and throws up an error. In the error "no matching function" basically means "no matching function that is The One Best Choice". When there's no best choice, the standard says that the statement is ill-formed and the compiler puts up an error.
It would appear that the other compilers might never even consider the first of these two forms of direct initialization as an option. They happily let you use the second one.
Scott
P.S. FWIW, the std::auto_ptr<T> works around this particular error message by declaring the initializer that takes a raw pointer as explicit... something like:
explicit std::auto_ptr<T>(T *rawPtr = 0);
By making the raw pointer initializer explicit, it takes that constructor off the table leaving only the case where a temporary is explicitly created. However, as you pointed out, the temporary cannot be bound to a non-const reference (the only direct-initializer left) so doing something like this:
std::auto_ptr<int> myPtr = new int();
Gives you a simpler error message, but it's still invalid syntax.