C++: When should you use copy constructors?

LET’S TALK COPY CONSTRUCTORS.

Well, first, let’s do a refresher course.

1. Copy constructors are when you construct a new instance of a class by giving it an already-constructed instance of that class to clone. MyClass otherMyClass = thisMyClass; and MyClass otherMyClass(thisMyClass); are the two ways to call copy constructors. At the end of copy construction, otherMyClass is theoretically the same as thisMyClass.

2. If you don’t explicitly create a copy constructor for your class (the footprint to use in your header is MyClass(MyClass& copyFromMe) ) then C++ will always default-create one. This auto-generated default copy constructor just calls the copy constructors of all member variables. The default copy constructor for primitive types (int, float, etc.) is just a data-copy.

3. Default copy construction of pointers is a shallow copy — you copy the pointer, but not the data it points to.

4. Copy constructors can be called implicitly. When you pass-by-value into a function, what that actually means is “copy-construct clones of the variables you want to pass in”. void DoStuffWith(MyClass thisMyClass) implicitly does a copy-construct on thisMyClass every time you call it.

5. (A sidebar, but useful to know) MyClass otherMyClass = thisMyClass; and MyClass otherMyClass(thisMyClass);, despite looking different, both call your defined MyClass(MyClass& copyFromMe) function. MyClass otherMyClass = thisMyClass; doesn’t call your operator=(MyClass& copyFromMe) function because that function is meant to be used on already-initialized instances of your class (copy assignment, not copy construction). MyClass otherMyClass; otherMyClass = thisMyClass; will default-construct otherMyClass and then call operator=.

So: when should you use copy constructors? Well, only when you really know what you’re doing, and you probably shouldn’t anyhow.

Let’s see by example, using the two classes below. They both hold a lot of memory; one statically allocates it, one dynamically allocates it.


class MyStaticAllocMem {
public:
   int m_iBuffer[1920*1080];
};

class MyDynamicAllocMem {
public:
   MyDynamicAllocMem() { m_pBuffer = new int[1920*1080]; }
   ~MyDynamicAllocMem() { delete[] m_pBuffer; }

   int* m_pBuffer;
};

void ActOnStaticAllocMem( MyStaticAllocMem staticMemValCpy ) { /* do stuff */ };
void ActOnDynamicAllocMem( MyDynamicAllocMem dynamicMemValCpy ) { /* do stuff */ };

void main()
{
   MyStaticAllocMem staticMem;
   MyDynamicAllocMem dynamicMem;

   ActOnStaticAllocMem( staticMem );
   ActOnDynamicAllocMem( dynamicMem );
}

When you call ActOnStaticAllocMem( staticMem ), the copy constructor generates a clone of staticMem to use inside the function. This means it stack-allocates another 1920*1080 ints and copies over all 7.9mb of data. This is very, very slow. Plus, if one stack-allocated MyStaticAllocMem is enough to make you worry about stack overflow, well, now you have two!

Still, your program can copy-construct static-allocated memory and survive. It’ll run significantly slower and be more prone to certain errors, but it runs. The same can not be said about copy-constructing dynamic-allocated memory.

When you call ActOnDynamicAllocMem( dynamicMem ), the copy constructor generates a clone of dynamicMem to use inside the function. This only stack-allocates the memory for one pointer, which isn’t scary — what’s scary is this:

  • When ActOnDynamicAllocMem returns, its function-scope variable dynamicMemValCpy goes out of scope, calling dynamicMemValCpy‘s destructor
  • Because the default copy constructor is a shallow copy, dynamicMemValCpy has a pointer that points to the same location as your dynamicMem back in main() points to
  • dynamicMemValCpy dutifully deletes its m_pBuffer, and the function returns, leaving you back at main() with dynamicMem pointing to unallocated data
  • The next time you do anything with dynamicMem, the world explodes.

There are plenty of workarounds to these issues, of course. You could use reference-counted pointers, or you could pass everything by reference or const-reference or by pointer (you really should do that). But it’s so easy to accidentally write void MyFunc(MyClass val) instead of void MyFunc(const MyClass& val), and the compiler will never complain.

Now, there are times where copy constructors are super-helpful — you may legitimately want a throwaway clone of a given object. Then, you can mess with the clone’s data as much as you want, and there are no repercussions after the function exits. Copy-constructing a class with sizeof(myClass) less than sizeof(void*) may also be faster than passing a pointer or reference (I totally haven’t tested that, though).

And if you don’t want to use copy construction, you can disallow it on a per-class basis using private: MyClass(const MyClass&).

But still, given how easy it is to implicitly copy construct, and given the amount of ways implicit copy construction can kill you / the amount of engineering needed to ensure it doesn’t, I’m surprised it’s implemented in C++ as an opt-out feature instead of an opt-in feature.

TL;DR — I do not like copy constructors.

3 thoughts on “C++: When should you use copy constructors?

  1. Christoph

    Copy constructors are when you construct a class by giving it another class to clone.

    Omg I just got cancer reading this. C++ isn’t javascript … you clone an object not a class dude.

    Reply
    1. Dave

      What are you talking about? He specifically says “instance of” a class. Unless he fixed it after reading your comment, you can disregard this reply.

      Reply

Leave a Reply

Your email address will not be published.