7

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?

Oskar
  • 889
  • 7
  • 20
  • Copy-constructors are, specifically, meant to copy the object. Adding additional method to copy the object is, in my opinion, superfluous. In cases, where you don't want to copy the object, when passing the instance to the function: pass by const-ref (e.g. `void some_method(Texture const& t)`) – Algirdas Preidžius Nov 06 '18 at 10:42
  • In the sample function you provide, it might make more sense to get a reference (`Texture&` or `const Texture&` depending on what you need) since it would avoid the copy. – Vivick Nov 06 '18 at 10:43
  • @Oskar We generally just say "that's the user's fault". They're a C++ programmer, after all, and are expected to think! – Lightness Races in Orbit Nov 06 '18 at 10:45
  • There are techniques available to manage this sort of situation - commonly used where copying would be expensive, or frequently called, and where items may well not need to be altered - Copy-On-Write https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Copy-on-write This might well be applicable here. – Rags Nov 06 '18 at 10:47
  • 3
    If copying is necessary, and can only be implemented in an "expensive" way, them's the breaks. It is necessary to avoid creating copies (e.g. pass by reference rather than by value) rather than avoiding a copy constructor. If it never makes sense to copy an object (being "expensive" on its own does not justify that) then disable or (C++11 and later) delete the copy constructor. It is also possible to find where copy constructors are used e.g. temporarily delete the copy constructor or make it private, to trigger a diagnostic when it is used - so unintended copies can be found and corrected. – Peter Nov 06 '18 at 10:55

3 Answers3

7

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

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • While this doesn't answer the question (which is 99.9% opinion based anyway, so there are no good answers), presents a nice idea, +1. Fortunately the SO police is slow to close the question, so we got this answer. – geza Nov 06 '18 at 11:01
  • I appreciate that the default copy constructor may have internal use, and so you may want to keep it - but it is also possible to declare it deleted, so it can never be called (in C++11 onwards, as I recall.) – Rags Nov 06 '18 at 11:10
  • @Rags - But the OP wants the class to be copyable, just not implicitly. – StoryTeller - Unslander Monica Nov 06 '18 at 11:21
  • @StoryTeller For sure - but a friend copy function has been added for that, which, as things stand, call the default copy. But if it shouldn't be a shallow copy (and with big complex objects that's often true) it can make sense to explicitly delete the default and use a different (private) function. Just a design choice. Sometimes it can be clearer to delete the default explicitly. Certainly not a requirement though. – Rags Nov 06 '18 at 11:31
  • @Rags defaulted constructors are just for exposition here. Using a member function to implement the copy sounds strange, though -- how are you going to initialize the values correctly? Private constructor with all parameters? – Quentin Nov 06 '18 at 12:34
  • @Quentin - a private constructor is certainly a neat and clear way. One of the beauties (and dangers) of C++ being the flexibility, and multiple ways of implementing things. I'm not being proscriptive - just noting possibilities. – Rags Nov 06 '18 at 13:11
5

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.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • 3
    A hack one can employ here besides the comment is to (ab)use the [`[[deprecated]]`](https://en.cppreference.com/w/cpp/language/attributes/deprecated) attribute with a message echoing that comment. But yeah, this answer is the way to go IMO too. – StoryTeller - Unslander Monica Nov 06 '18 at 10:45
  • *Damn* I hate iostreams. Such a towering pile of templates and polymorphism and you can't even move them meaningfully :( – Quentin Nov 06 '18 at 10:46
  • 1
    @Quentin Just this week I find myself fighting with streambuf derivations to make them do what I want (a fairly simple thing) - gah! – Lightness Races in Orbit Nov 06 '18 at 10:58
0

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.

Lie Ryan
  • 62,238
  • 13
  • 100
  • 144
  • 1
    "_A shallow copy should be very fast_" In that case, which copy of the object should be responsible to free the allocated memory? – Algirdas Preidžius Nov 06 '18 at 10:52
  • @AlgirdasPreidzius: Just like basic pointers, you can leave it to the caller to decide. Or if you want to be fancy, you can emulate the behaviour of shared_ptr and do reference counting, or move the ownership like unique_ptr. – Lie Ryan Nov 06 '18 at 11:08
  • How can a caller decide? A copy constructor is just a copy constructor. After copy-constructor is executed, there's no way to know which "copy" is the original. And your answer, doesn't touch on the topic, of who is supposed to free the memory (and the possible techniques to do so), that was shallow-copied. – Algirdas Preidžius Nov 06 '18 at 11:20
  • @AlgirdasPreidzius: you're making it as if that question was never asked by pretty much everyone who used pointers. If you want to make it act like a basic pointer, then just like all basic pointer, it needs to be deleted manually and it's the programmer's responsibility to decide which copy is original and when and where it is appropriate to delete. Or if you want to make it have a smart pointer semantic, there's already a well established semantic, mechanism, and idioms for doing so. My point is, there's no need to invent new copy semantic. – Lie Ryan Nov 06 '18 at 11:31
  • You seem to misunderstand me. Even if I agree with the fact, that there's no reason to reinvent the wheel, regarding smart pointer semantic, your answer touches none of that. Your answer reads as if you can just copy-over the pointer (perform shallow-copy), without touching on the topic of freeing such memory, and all relevant information, regarding the answer, must be present in the answer itself, and not in the comments bellow it. – Algirdas Preidžius Nov 06 '18 at 11:54