In C++, if copying an object is possible, but really expensive and usually a bad idea, should you still implement copying using a copy constructor?

Viewed 1050

I'm pretty new to C++, and I have a question regarding some C++ conventions regarding copying. I've googled around and haven't really been able to find good guidance on this, so I'm turning to you fine folks.

Let say you have an object that represents some resource that is technically copyable, but the copy is a expensive and almost always the wrong thing to do. Should you still implement a copy constructor for it? Or is it better to make a member function that is something like make_copy() (for those rare times when you DO want to copy the object).

For instance: lets say you have a class representing a texture stored in video memory. This resource is technically copyable: you can create a new handle for it and copy the memory (either through the CPU or using graphics library calls). But generally speaking, this is not something you really want to do very often. It's expensive, and it's usually the wrong thing to do, and is potentially very wasteful of memory. However, you could imagine corner cases where it would make sense: taking a screenshot and applying filters over it or something. Those cases would be rare, but they would exist.

The reason I'm hesitant making a copy constructor that does this this is that I feel like C++ is a bit too eager to copy stuff. Maybe it reflects the fact that I'm a bit new to the language, but since C++ will call the copy constructor in all sorts of situations where you might not mean for it to do so. Like:

void some_method(Texture t)
{
    ...
}

Texture t(<arguments>);
Texture t2 = t;        // calls copy constructor

some_method(t2);       // calls copy constructor

I would much rather these two lines that calls copy constructors be compiler errors (because I feel like they're very easy mistakes to make) and if you want to actually make a copy, you need to be very explicit about it and use a dedicated member function.

Is there some standard practice around this? Some sage advice on when you should (and shouldn't) write copy constructors? Some Scott Meyers chapter I've missed? Or should I just do it whenever it's possible?

EDIT: to be clear about my example: obviously, you should pass the argument by reference, but my point was that this is a very easy mistake to make, just leaving out the ampersand. And if you do that, the compiler will happily replace a cheap pass-by-reference with a very expensive copy, and I would rather the compiler not do that. But I dunno, maybe this isn't a mistake people generally make in C++, and I'm being overly cautious?

3 Answers

Whether you want to use it is your call, but here is a pattern that is relatively light on syntax:

struct ExplicitCopy {

    // Implement as usual
    ExplicitCopy() = default;
    ExplicitCopy(ExplicitCopy &&) = default;
    ExplicitCopy &operator = (ExplicitCopy &&) = default;

    // Copying happens with an ADL call to this function
    friend ExplicitCopy copy(ExplicitCopy const &orig) {
        return orig;
    }

private:
    // Copy operations are private and can't be called accidentally
    ExplicitCopy(ExplicitCopy const &) = default;
    ExplicitCopy &operator = (ExplicitCopy const &) = default;
};

Then, trying to copy an instance will result in a compiler error from the call to a private constructor.

ExplicitCopy ec;
ExplicitCopy ec2 = ec;       // Nope
ExplicitCopy ec3(ec);        // Nope
ExplicitCopy ec4 = copy(ec); // Yes

Thanks to copy elision, there is no additional constructor call.

See it live on Coliru

This is kind of a matter of opinion, but if copying makes sense (despite its cost) then I would

  • provide a copy constructor, noting in comments that it's expensive (which should encourage users of your class to pass by reference where appropriate)
  • provide a move constructor (which allows users of your class to do things "on the cheap")

Don't forget assignment operators for each case.

And if you follow the rule of zero then the language will do all of this for you.

An example is std::vector. Did you really want to copy 50,000 heavy vector elements? Maybe not, but at least you had the choice to do so.

Counter-examples include standard streams, which are not copyable… but this is because it literally makes no sense to copy a "stream of data" (streams aren't containers!). They're also not moveable, but that's another story.

a class representing a texture stored in video memory.

That sounds like a pointer, so I think it's not unreasonable to expect it should have a pointer semantic for copying, i.e. it does a shallow copy by default. A shallow copy should be very fast, and shouldn't require copying the actual texture in graphic memory, but rather that they become alias for the same texture in GPU memory.

Related