Friday, December 26, 2014

Movable classes are containers

If you're writing C++11 code, you should be aware that a move constructor and move assignment operator are automatically generated if you write your classes properly. However, there are cases when making a class movable can lead to subtle bugs. This article explains the following:
  1. When we might want to store a pointer to this.
  2. Why storing a pointer to this makes it difficult to implement move operations.
  3. How move operations can be made to work using containers.
  4. Why Movable classes are containers, and why this is a problem.
Note: There exists a definition of the word "container" used by the C++ standard libraries that refers specifically to things like arrays and vectors. In this article, I use the term "container" to refer to any class whose purpose is to contain a pointer to object(s) in one way or other.
Without further ado, let us begin.

 Part 1: If only things were always this simple

Consider the following (simplified) example:
struct Node
{
    Node* Self = this;
};
In this case, it would not be correct to copy an instance of Node because the Self pointer would not be updated to point to the copied instance. There are solutions to this problem: You could disable copying of Nodes entirely (a common choice), or you could implement a custom copy constructor and copy assignment operator that correctly updates the Self pointer. The move operations can be implemented similarly, so in this case there's no real problem. However, as I'll explain in the rest of the article, things get messy when they get more complicated.

Part 2: Where things get ugly

A less obvious incarnation of this problem is when you use the popular C userdata idiom. Consider the following Win32 programming example:
struct Window
{
    HWND NativeHandle;

    Window()
    {
        NativeHandle = CreateWindow(...);

        /* more initialization code goes here */

        SetWindowLongPtr(NativeHandle, GWLP_USERDATA, (LONG_PTR) this);
    }
};
This popular pattern is used to store a pointer to our custom Window class within the Win32 native window handle. This idiom is useful because Win32 events come with the HWND ("Handle to a Window") that emitted them, which we can use to find a pointer to our own Window class. SetWindowLongPtr is the Win32 API function that allows you to specify the pointer to store with the native handle.

You might have noticed that this suffers from the same problem as the simplified example at the start of this article, but it has suddenly become much more difficult to solve the problem. We can no longer implement a hand-written copy constructor because copying a complex opaque resource like a HWND is non-trivial. If we really wanted to, we could actually still implement a move constructor and move assignment operator as long as we are careful to call SetWindowLongPtr to update the handle to point to its new owner. However, this is unwise: According to the MSDN documentation, SetWindowLongPtr is a function that can fail to execute. This is problematic because the design of C++ suggests that move operations should be noexcept. In other words, move operations should never fail.

You could argue that in your system SetWindowLongPtr would never fail in practice, but this is not a general solution to the problem. There must certainly exist similar cases out there where updating the userdata is a non-trivial operation which we can't guarantee will succeed. The only safe solution is to disable move operations entirely, which makes this class much more difficult to use because of how useful move operations are. Thus, in the next part, I will explain how to make it possible to move again.

Part 3: Containers are the solution (and the problem)

The problem in part 2 can be solved, like all other problems in computer science, by another layer of indirection. If we make our Window class a dynamically allocated member of another class, then this new container class can guarantee that the address of our Window instance does not change. It can properly disable copying, and it can safely implement move operations by (for example), swapping the contained pointers to Window instances. Here is an example of such a class:
struct WindowContainer
{
    unique_ptr<Window> ContainedWindow = make_unique<Window>();
};
(Aside: You could skip WindowContainer and just use a plain unique_ptr, but having WindowContainer makes it possible to give it convenient member functions that forward the calls to the contained Window.)

We have now solved the problem of ensuring that the address of our Window instances stay the same (by disabling both copy and move operations), while still making it possible to use move operations by using the WindowContainer instead. However, this has some disadvantages: First, additional dynamic allocations and indirect lookups are required to implement WindowContainer. Second, a pointer to a WindowContainer can't be used to represent the identity of a Window.

That second point is actually pretty annoying. It means that storing a pointer to a WindowContainer is meaningless, because WindowContainers are only used as a vehicle to transport the actual resource (the Window.) In other words, storing a pointer to a WindowContainer is pretty much meaningless because the Window it contains can be moved into a different WindowContainer with a different address. This identity problem actually affects any movable class that contains a resource, which brings me to my final point.

Part 4: Movable classes are containers

If a class is designed to be movable, then a pointer to it can no longer be used to represent the identity of what it contains. This is because its identity can freely be moved into a different object. This problem affects movable classes in general, except perhaps classes where the move operations are implemented simply as a copy (like a POD array.)

It would be a mistake to create a public API that only exposes a single Window class that behaves like WindowContainer, unless you also provide a way to refer to the identity of a Window (which becomes, in my opinion, an unnecessary complication.)

In Conclusion

Because movable classes are disconnected from their identity, it can be concluded that movable classes are containers rather than being resources themselves. Thus, if you create a class that is supposed to represent a resource, it should be neither copyable nor movable. If you want to move it, you should wrap it in a container class like a smart pointer or a standard container. This makes the relationship between container and containee explicit, and makes potential bugs more obvious to users of the API.

Appendix

 I talked a lot about disabling copy and move operations but never showed an example. Here's how you can do such a thing in C++11:
struct NoCopyNoMove
{
    // Disables the copy constructor and copy assignment operator.
    NoCopyNoMove(const NoCopyNoMove&) = delete;
    NoCopyNoMove& operator=(const NoCopyNoMove&) = delete;

    // Disables the move constructor and move assignment operator.
    NoCopyNoMove(NoCopyNoMove&&) = delete;
    NoCopyNoMove& operator=(NoCopyNoMove&&) = delete;
};

4 comments:

  1. Of course a moveable class is a container, there's nothing new in your post I think (Don't get me wrong, I enjoyed it a lot). In most of the cases a C++ is not a resource but something using/managing a resource. That's what the RAII idiom talks about.

    Thinking that the resource used/managed by a C++ object is (or is part of) the identity of an object is a very big mistake. For the same reason, taking a pointer to the object as a identifier of the resource is an error. One thing is who uses the resource, and other the resource itself. Of course resources can be passed freely between objects since they are using them, resources are not part of their identity.

    In the old days of C++98/03 this semantic/philosophical aspect was not a problem since the language has no semantics to pass resources: A resource was completely attached to an object, if you wanted to pass a resource you should own a new one in your own object with exactly the same properties. There was not the same resource, but a new one behaving exactly the same.

    Since C++11 people realized that resources and objects are different and detached things, since resources can fly between objects. But that's nothing new, it was more obscure before only.

    Resources are not part of objects identity and value (move, copy, etc) semantics of objects should be implemented in that way. And we shouldn't assume that an object is the resource it manages. It's not.

    ReplyDelete
    Replies
    1. A good question is: If resources are not part of object's identity, from a set of member variables of an object what are part of the state of the object and what are resources used by the object? Is the state part of the object's identity?

      Delete
    2. It sounds similar to the idea of value types and reference types

      Delete
  2. Maybe a good point. But considering move operation as a optimized copy, this situation isn't really new, and existed before C++11. What about that?

    ReplyDelete