Generated Copy Constructors Considered Evil

Sometimes I really hate C++. Not just dislike it, but really, really hate it. This week, one of the most horrible language “features” got me again: the generated copy constructor. I understand why they exist — they’re necessary to allow C structures to be passed by value no extra effort. However, their behaviour causes a world of pain that should never have been inflicted on developers.

I have a template class — let’s call it Foo. It used to have a couple of non-trivial constructors and assignment operators:

template <typename T>
class Foo
{
public:
    Foo(T* = 0);
    Foo(const Foo<T>&);
    ~Foo() throw();

    Foo<T>& operator=(T*);
    Foo<T>& operator=(const Foo<T>&);

    ...
};

This all works fine — user-defined constructors and assignment operators are used in all cases. But one day, I realise that I can simplify some code by making the constructors and assignment operators more general:

template <typename T>
class Foo
{
public:
    Foo(T* = 0);
    template <typename U>
    Foo(const Foo<U>&);
    ~Foo() throw();

    Foo<T>& operator=(T*);
    template <typename U>
    Foo<T>& operator=(const Foo<U>&);

    ...
};

Instead just being able to construct or assign from the same class, you should be able to construct or assign from any instantiation of the template. But this caused things to break all over the place. Can you see why? The compiler will now generate a copy constructor and assignment operator. To stop the compiler from generating them, you need not just a constructor/operator general enough to accept an instance of the same class, but a constructor/operator that takes an instance of exactly the same class. To make it work, I need to do this:

template <typename T>
class Foo
{
public:
    Foo(T* = 0);
    Foo(const Foo<T>&);
    template <typename U>
    Foo(const Foo<U>&);
    ~Foo() throw();

    Foo<T>& operator=(T*);
    Foo<T>& operator=(const Foo<T>&);
    template <typename U>
    Foo<T>& operator=(const Foo<U>&);

    ...
};

There’s another case where this can easily trip you up. Consider this:

class Fish
{
    ...
};

class Salmon : public Fish
{
public:
    Salmon(const Fish&);

    ...
};

Counter-intuitively, a generated copy constructor will be used to construct Salmon from other instances of Salmon derived classes; the user-defined constructor will only be used to construct Salmon from instances of Fish and other derived classes thereof.

The current counter-intuitive behaviour makes it too easy to end up with broken code. The issues could have been avoided in a number of ways:

  • No generated copy constructors/assignment operators
  • Suppress generated copy constructors/assignment operators in the presence of user-defined copy constructors/assignment that are general enough to accept an instance of the same type (or to think of it another way, give the generated copy constructor/assignment operator lower precedence than all user-defined constructors/assignment operators)
  • Suppress generated copy constructors/assignment operators
    in the presence of any user-defined constructors/assignment operators

While I’m excited about some of the new features in C++0x, I can’t help but dread that some of them will be implemented in equally brain-dead ways. Move semantics is one that comes to mind immediately.

This entry was posted on Saturday, 4 April, 2009 at 1:40 pm and is filed under C, Development, Technology. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

Leave a Reply