0

Is it acceptable C++ style to let classes that wrap shared handles be copyable?

Very frequently I find myself writing classes that hide the details of some gnarly C library or OS resource by holding a shared_ptr to it behind the class interface. E.g.

class window
{
public:
     window() : m_handle(OsCreateWindow(), OsDestroyWindow) {}

     void make_it_dance();
     void paint_it_red();
private:
     shared_ptr<OS_WINDOW_HANDLE> m_handle;
}

Because the classes are copyable and shared_ptr does the hard work, the instances can be passed around willy-nilly and nothing leaks or is destroyed twice. So, technically, all is good. I've been doing this for years.

But recently I've started to wonder whether this is really good style. After all, the object at the end of the handle isn't being duplicated when the class instance is. Would it be better to make all these classes non-copyable, making very clear to users that they're dealing with a reference to the same object? Modern C++ makes a big deal of being "value-based", and sharing the backend resource between instances seems to go against that principle.

However, the consequence would be that a large proportion of my code would deal in pointers, even if they are smart pointers. That seems a step backwards.

Craig M. Brandenburg
  • 3,354
  • 5
  • 25
  • 37
thehouse
  • 7,957
  • 7
  • 33
  • 32

4 Answers4

2

I think I understand what your dilemma is. And I have a rather silly suggestion.

Since your problem is not functional, but rather with the expectation that copying your window instance creates, would it not be better if you name such a class as this a window_handle instead of window?

This will imply that that is merely a handle to some window and copying it does not create a new window or anything like that and it's merely duplicating a handle.

To emphasize, I'm suggesting that you keep your design (which seems a good one to me and seems to be working out for you) and just change you naming to change the expectation of higher layers of the code.

yzt
  • 8,873
  • 1
  • 35
  • 44
  • This is a not such a silly suggestion. But I'm interested in the implications of "value-based" C++ that expect copied instances to be independent of each other. – thehouse Jun 27 '13 at 00:02
  • At first I though this window handle idea would just parallel `shared_ptr`, which has value semantics (operations on one copy don't visibly affect the behaviour of the others) but then I realised that doesn't apply to `window_handle` as it has the full window interface and non-const operations will definitely affect the behaviour of others. The solution would be to give copyable `window_handle` an accessor method returning a non-copyable `window`, where the latter has the rest of the interface. But by that point I've basically got a `shared_ptr` typedef. – thehouse Jun 27 '13 at 00:06
  • @thehouse: I'm sure people more knowledgeable and experienced than I would be able to help you there. But from my perspective, there is nothing wrong with handle/smart pointer/ID classes and objects. After all, a `shared_ptr` is exactly such a class (its copied instances are *not* independent of each other) and your class merely wraps a `shared_ptr`. – yzt Jun 27 '13 at 00:07
  • @thehouse: Consider the case of iterators. They are similar to your case in that they are light-weight objects pointing into a container. And everybody understands that you can change a container (change an element, erase one and reallocate the whole thing, etc.) via an iterator. It is understood and accepted. Again, this is the case with `shared_ptr` too. You can change the underlying object via a `shared_ptr` and everybody understands and accepts that. So, while your points about value semantics are valid, they are not universal; the standard library itself contains a few exceptions. – yzt Jun 27 '13 at 00:19
  • If you read my other reply you'll see I had the same though, but that's it's not actually true. One `shared_ptr` and it's copy cannot change the observable behaviour of it's methods (ok, except for two that reveal the underlying reference count so I guess it's not 100%). The shared pointee can have its behaviour changed and, but that's a change in the `shared_ptr` value. The `window_handle` is not like `shared_ptr` in that is directly has the window methods in its interface. An operation on one copy changes those behaviours on another. – thehouse Jun 27 '13 at 00:28
  • I'm not entirely clear what the rules are for interators. I would have said that two copies of the iterator can modify the same collection but can't modify the observable behaviour of each other's methods. But then what about `operator*`? If the iterators are at the same posision and the underlying collection is changed via one of them, is the second iterator going to return that new value from operator*? – thehouse Jun 27 '13 at 00:33
  • @thehouse: I think I see what you mean. But I would call it syntactical differences, and not semantical (and I might be wrong!) Whether you write `shared_ptr_to_window->resizeWindow()` or `handle_to_window.resizeWindow()` doesn't seem that important to me. This is what you are talking about, are you not? (And I do get the deeper meanings and notational contracts that you are talking about. I just don't think that they are *that* important here.) – yzt Jun 27 '13 at 00:48
0

Personally, I wouldn't allow copying and agree that using a shared pointer is a step backwards. I would also add that each "Window" instance should contain a unique OS_WINDOW_HANDLE.

You could also employ the MFC approach of passing handles between objects. MFC uses Attach and Detach methods something like the following...

class Window
{
public:
  void Attach (OS_WINDOW_HANDLE handle)
  {
    ASSERT(NULL == m_handle);  // or you could destroy the existing handle
    m_handle = handle;
  }

  OS_WINDOW_HANDLE Detach()
  {
    OS_WINDOW_HANDLE retVal = m_handle;
    m_handle = NULL;
    return retVal;
  }

private:
  // disable copy constructor and assignment
  Window(const Window&);
  Window& operator=(const Window&);
};
0

If the resource a handle points to does not need to be duplicated (shallow copy) use a std::shared_ptr. If the resource does need to be duplicated (deep copy) you should use std::unique_ptr. If no copying needs to be done use std::unique_ptr.

Captain Obvlious
  • 19,754
  • 5
  • 44
  • 74
  • Are you suggesting these as ways to achieve the different behaviours I've described? I know how to get the behaviours. The question is about which behaviours are better. – thehouse Jun 26 '13 at 23:48
0

A possible answer (and the situation that prompted the question) is that silently sharing the backing object makes it hard for the user of the class to make sure that the object is 'dead' at a particular point. For example, the object may be a network connection and the user needs to know for security reasons that the connection has been closed.

If the connection wrapper is copyable and shares the connection between instances, then the user has to study the paths the instance may flow along to make sure a copy can't be saved somewhere unexpected and hold the connection open.

On the other hand, if the wrapper is non-copyable, there is only one instance to make sure is dead. References may still be handed out, but once the original wrapper dies, the user can be sure the backing object is dead too.

If the user wants to have shared copies, they can always reinstate that with a shared_ptr again. But this time the decision is theirs.

thehouse
  • 7,957
  • 7
  • 33
  • 32