10

I am wondering what possible merits does copy-on-write have? Naturally, I don't expect personal opinions, but real-world practical scenarios where it can be technically and practically beneficial in a tangible way. And by tangible I mean something more than saving you the typing of a & character.

To clarify, this question is in the context of datatypes, where assignment or copy construction creates an implicit shallow copy, but modifications to it creates an implicit deep copy and applies the changes to it instead of the original object.

The reason I am asking is I don't seem to find any merits of having COW as a default implicit behavior. I use Qt, which has COW implemented for a lot of the datatypes, practically all which have some underlying dynamically allocated storage. But how does it really benefit the user?

An example:

QString s("some text");
QString s1 = s; // now both s and s1 internally use the same resource

qDebug() << s1; // const operation, nothing changes
s1[o] = z; // s1 "detaches" from s, allocates new storage and modifies first character
           // s is still "some text"

What do we win by using COW in this example?

If all we intend to do is use const operations, s1 is redundant, might as well use s.

If we intend to change the value, then COW only delays the resource copy until the first non-const operation, at the (albeit minimal) cost of incrementing the ref count for the implicit sharing and detaching from the shared storage. It does look like all the overhead involved in COW is pointless.

It is not much different in the context of parameter passing - if you don't intend to modify the value, pass as const reference, if you do want to modify, you either make an implicit deep copy if you don't want to modify the original object, or pass by reference if you want to modify it. Again COW seems like needless overhead that doesn't achieve anything, and only adds a limitation that you cannot modify the original value even if you want to, as any change will detach from the original object.

So depending on whether you know about COW or are oblivious to it, it may either result in code with obscure intent and needless overhead, or completely confusing behavior which doesn't match the expectations and leaves you scratching your head.

To me it seems that there are more efficient and more readable solutions whether you want to avoid an unnecessary deep copy, or you intend to make one. So where is the practical benefit from COW? I assume there must be some benefit since in it used in such a popular and powerful framework.

Furthermore, from what I've read, COW is now explicitly forbidden in the C++ standard library. Don't know whether the con's I see in it have something to do with it, but either way, there must be a reason for this.

15

Copy on write is used in situations where you very often will create a copy of the object and not modify it. In those situations, it pays for itself.

As you mentioned, you can pass a const object, and in many cases that is sufficient. However, const only guarantees that the caller can't mutate it (unless they const_cast, of course). It does not handle multithreading cases and it does not handle cases where there are callbacks (which might mutate the original object). Passing a COW object by value puts the challenges of managing these details on the API developer, rather than the API user.

The new rules for C+11 forbid COW for std::string in particular. Iterators on a string must be invalidated if the backing buffer is detached. If the iterator was being implemented as a char* (As opposed to a string* and an index), this iterators are no longer valid. The C++ community had to decide how often iterators could be invalidated, and the decision was that operator[] should not be one of those cases. operator[] on a std::string returns a char&, which may be modified. Thus, operator[] would need to detach the string, invalidating iterators. This was deemed to be a poor trade, and unlike functions like end() and cend(), there's no way to ask for the const version of operator[] short of const casting the string. (related).

COW is still alive and well outside of the STL. In particular, I have found it very useful in cases where it is unreasonable for a user of my APIs to expect that there's some heavyweight object behind what appears to be a very lightweight object. I may wish to use COW in the background to ensure they never have to be concerned with such implementation details.

  • Mutating the same string in multiple threads seems like a very bad design, regardless of whether you use iterators or the[] operator. So COW enables bad design - that doesn't sound like much of a benefit :) The point in the last paragraph seems valid, but I myself am not a great fan of implicit behavior - people tend to take it for granted, and then have a hard time figuring out why code doesn't work as expected, and keep wondering until they figure out to check what is hidden behind the implicit behavior. – dtech Nov 27 '15 at 14:12
  • As for the point of using const_cast seems like it can break COW just as easily as it can break passing by const reference. For example, QString::constData() returns a const QChar * - const_cast that and COW collapses - you will mutate the original object's data. – dtech Nov 27 '15 at 14:14
  • If you can return data from a COW, you must either detach before doing so, or return the data in a form which is still COW aware (a char* obviously is not aware). As for the implicit behavior, I think you're right, there are issues with it. API design is a constant balance between the two extremes. Too implicit, and people start relying on special behavior as though it was de facto part of the spec. Too explicit, and the API becomes too unwieldy as you expose too many underlying details that weren't really important, and are suddenly written into your API spec. – Cort Ammon Nov 27 '15 at 17:39
  • I believe the string classes got COW behavior because the compiler designers noticed that a large body of code was copying strings rather than using const-reference. If they added COW, they could optimize this case and make more people happy (and it was legal, until C++11). I appreciate their position: while I always pass my strings by const reference, I had seeing all that syntactic junk that just detracts from readability. I hate writing const std::shared_ptr<const std::string>& just to capture the correct semantics! – Cort Ammon Nov 27 '15 at 17:41
5

For strings and such it seems like it would pessimize more common use cases than not, as the common case for strings is often small strings, and there the overhead of COW would tend to far outweigh the cost of simply copying the small string. A small buffer optimization makes much more sense to me there to avoid the heap allocation in such cases instead of the string copies.

If you have a heftier object, however, like an android, and you wanted to copy it and just replace its cybernetic arm, COW seems quite reasonable as a way to keep a mutable syntax while avoiding the need to deep copy the entire android just to give the copy a unique arm. Making it just immutable as a persistent data structure at that point might be superior, but a "partial COW" applied on individual android parts seems reasonable for these cases.

In such a case the two copies of the android would share/instance the same torso, legs, feet, head, neck, shoulders, pelvis, etc. The only data which would be different between them and not shared is the arm which was made unique for the second android on overwriting its arm.

  • This is all good, but it doesn't demand COW, and is still subject to a lot of harmful implicity. Also, there is a downside to it - you may often want to do object instancing, and I don't mean type instancing, but copy an object as an instance, thus when you modify the source object, the copies are also updated. COW simply excludes that possibility, as any change to a "shared" object detaches it. – dtech Dec 11 '15 at 10:35
  • Correctness IMO should not be "easy" to achieve, not with implicit behavior. A good example of correctness is CONST correctness, as it is explicit and leaves no room for ambiguities or invisible side effects. Having something like this "easy" and automatic never builds up that extra level of understanding of how things work, which is not only important to overall productivity, but pretty much eliminates the possibility of undesired behavior, the reason for which might be hard to pinpoint. Everything made possible implicitly with COW is easy to achieve explicitly as well, and it is more clear. – dtech Dec 11 '15 at 10:39
  • My question was motivated by a dilemma whether or not to provide COW by default in the language I am working on. After weighting the pro's and con's, I decided to not have it by default, but as a modifier that can be applied to both new or already existing types. Seems like the best of both worlds, you can still have the implicitness of COW when you are explicit about wanting it. – dtech Dec 11 '15 at 10:54
  • @ddriver What we have is something akin to a programming language with the nodal paradigm, except for simplicity the nodes kind of use value semantics and no reference-type semantics (maybe somewhat akin to std::vector<std::string> before we had emplace_back and move semantics in C++11). But we're also basically using instancing. The node system may or may not modify the data. We have things like pass-through nodes which do nothing with the input but just output a copy (they're there for user organization of his program). In those cases, all the data is shallow copied for complex types... – user204677 Dec 11 '15 at 12:39
  • @ddriver Our copy-on-write is effectively a "make instance unique implicitly on change" kind of copying process. It makes it impossible to modify the original. If object A is copied and nothing is done to it to object B, it's a cheap shallow copy for complex data types like meshes. Now if we modify B, the data we modify in B becomes unique through COW, but A is untouched (except for some atomic reference counts). – user204677 Dec 11 '15 at 12:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.