Where's the proper (resource handling) Rule of Zero? [closed]
Asked Answered
K

2

11

Here's an article that talks about an idiom named Rule of Zero.

Here's an excerpt:

class module {
public:
    explicit module(std::wstring const& name)
    : handle { ::LoadLibrary(name.c_str()), &::FreeLibrary } {}

    // other module related functions go here

private:
    using module_handle = std::unique_ptr<void, decltype(&::FreeLibrary)>;

    module_handle handle;
};

It reuses unique_ptr RAII features so you don't need to care about implementing a daunting and verbose Rule of Five wrapper.

Presented this way (managing handle based resources with unique_ptr, that way), it looks as a hack for me, not a best solution for what it's trying to solve. Too many assumptions are being implicitly assumed:

  • One is able to peer and use the basic type the #define (or typedef) HANDLE is built upon. For me this should be hidden knowledge, and a solution be based exclusively on what the interface makes available: HANDLE.
  • Handles, can be anything, they're not required to be pointer types.

I'd like to use this idiom, but it falls short in many situations I stumble upon.

Is this handle focused RAII wrapper already done and usable in some cool library? Is everybody using such a tool and I'm not aware? (I think it nice to have such tooling not only for one, but for the many types of ownerships that are)

EDIT 1

This is not about platform specific resource handles, e.g., glGenLists returns a kind of handle, it's a GLuint, and you should call glDeleteLists on it. As already stated, resource handles are not required to be pointer types, and this assumption should not be assumed.

EDIT 2

Rule of Zero, in the former sample, by employing an already existing tool, unique_ptr, shows as a nice shortcut for handle management. The extra assumptions that it requires makes it falls short. The right assumptions are that you have a handle and you have a resource destroying function that destroys the resource given by the handle. Whether the handle is a void *, a GLuint, whatever, it should not matter, and worse, one should not even be required to peer HANDLE inner type. For the purpose of managing handles RAII way, if the idiom tells it's good for that, and can't apply in such situations, I feel it's not good for that, at last with the given tool.

EDIT 3

An illustrative situation would be, let's say you're in charge of using a fresh 3rd party C library. It contains a FooHandle create_foo() and a void destroy_foo(FooHandle). So you think, "let's use FooHandle's by employing the Rule of Zero". Ok, you go by using a unique_ptr, a unique_ptr to what you ask yourself? Is FooHandle a pointer?, so that you use a unique_ptr to FooHandle's not exposed basic type? Is it an int?, so that you may use it straight, or is it better to (re)typedef things like @NicolBolas has done in his answer. For me, it seems clear, even in such a trivial situation, unique_ptr is already showing as not an ideal fit for managing resource handles.

Disclaimer:

I've tried to reformulate and better express myself in:

EDIT 4

I've found what I was looking for, I've put it as an answer in the reformulated question: https://stackoverflow.com/a/14902921.

Kesler answered 14/2, 2013 at 0:39 Comment(19)
You failed to name a situation in which the idiom was not usable. And yeah, this "rule" is "already done". It's called modern C++ styleCitarella
The fact of the matter is that a HANDLE in the winapi is a void *. That will never change, and is clearly stated in the Windows Data Types article. I see absolutely no problem with using a smart pointer to easily employ RAII.Coercion
@Coercion It is not about handles, nor is it about the Winapi. It is about simplyfing C++ object value semantics. This has historically been a source of many bugs.Citarella
I don't understand the question at all. Perhaps I'm just too tired.Froufrou
@sehe, I was kind of going off of the two bulleted points in relation to the example.Coercion
This seems more rant than question.Prudenceprudent
You seem to be confusing the concept of the Rule Of Zero (try to reuse resource managing classes so you don't have to write new ones) with the existence of some generic wrapper. The code listed is just an example of using the Rule of Zero, it doesn't claim to be able to handle any possible handle type. Does a fully generic handle wrapper exist? Probably not.Repugn
Let me chime in as the author of the article. I fear I did not make myself clear enough. As GManNickG states, the idea of the Rule of Zero is not that all the ownership primitives you will ever need have already been written. The idea is that ownership primitives are the only thing that should be dealing with ownership. If there isn't a primitive that fits your goals, by all means, write one. Just don't mix ownership with other responsibilities. It's not an idiom: it's just the Single Responsibility Principle applied to a particular concern.Algology
@R.MartinhoFernandes, The thing is, your article clearly leads one to think c++, even more c++11, already contains the tools that fits well for a job like handle based resource management, so one hardly finds itself in a situation so trivial and still needs to write one. I know I can write my own "unique_handle" that does the job properly. But still, I'm writing it myself. For all I've have said, the excerpt I have taken is a bad example, unique_ptr doesn't look as a tool that looks Responsible for HANDLE's, employed that way. Nicol Bolas addressed one of the things I think as a problem well.Kesler
I don't solicite debate, I ask for an existing tool, if there's one already being used in the wide, that does this job more properly. Just that, I welcome suggestions and advices, like the one presented by Nicol Bolas, but the question is simple. I don't ask people to tell me to write my own tool for such a trivial situation, the question is whether such a trivial tool is already built, if no one is using such a tool, if currently everyone privately writes their own wrapper for such situations, OK, I then can accept I need to write my own myself. For this, I've voted for reopen.Kesler
@GManNickG, IMHO, the code listed is not just an example of employing the Rule of Zero, it's a bad one, I've already explained why it's. It would be much better to write a wrapper the does the job of managing the resource's lifetime and use this one as the basis for a good use of the Rule of Zero, not using unique_ptr like that, and make one think c++ is already good at such a kind of tool, so you may just employ them instead of writing one yourself.Kesler
The main point is yes you may have to write your own wrapper. The blog post you discuss never said otherwise. You can't extrapolate that from the single example given. Re. Edit 3 The 'not exposed base type' is bogus - C API's don't have virtual polymorphism anyway. The retypedef way is nice (it's called meta-programming or a type trait). So, yes, unique_ptr suits many more cases than a casual glance would suggest. And, yes, by all means, write a resource wrapper. That in no way negates the value of the Rule Of Zero article.Citarella
@sehe, fixed "base type" to "fundamental type". I meant basic fundamental type of the pointer, not base from "base class".Kesler
@sehe, and no, it strongly suggests I'll hardly need to write it: "Common “ownership-in-a-package” classes are included in the standard library: std::unique_ptr and std::shared_ptr. Through the use of custom deleter objects, both have been made flexible enough to manage virtually any kind of resource."Kesler
@sehe, and going by making use of such a example in the end, reinforces that statement and adds to misleading.Kesler
Even then, what could possibly be the matter with the pointed-to type being incomplete at the time of declaration? That's not an issue, especially as you're not invoking a (virtual) destructor through it: you're using the custom deletor. You usage is exactly what it would 'normally' be - but automated.Citarella
@chico I'm still awaiting your example where you couldn't use an existing 'ownership-in-a-package'. If your example turns out to be compelling situation that commonly arises, you may have a point. Right now, I can't think of an example. And you seem uninterested to supply it. Till then, I'll leave it at this.Citarella
@sehe, EDIT 3 is a completely trivial example, it can't be more simple than that.Kesler
@sehe, the issue of "being incomplete at the time of declaration" is that, through unique_ptr, if FooHandle is a pointer type, the unique_ptr should be constructed upon the basic type (like the excerpt has badly done) not straight from FooHandle alone. What if it turns out to be just an int? The semantics of handles are just like this, unique_ptr doesn't fit, it's a hack.Kesler
C
12

Background: Rule Of Zero

First and foremost, that article is about a much more general idea than just a the resource handle wrapper example it mentions.

The point is that whenever a class contains members with 'non-trivial' ownership semantics, the class should not be responsible for the mechanics of ensuring proper value semantics (copy. assign, move, destruct) of the containing class. Rather, each constituent should itself implement 'Rule-Of-3+' as appropriate, so that the composite class can leverage the compiler-defaulted special members.

Further more, the new standard smart pointer types greatly simplify the task of making this happen for the contained types. In most cases the members that required attention would be pointers: std::unique_ptr, shared_ptr, weak_ptr address the needs here.

For custom resource types you might have to write a Rule-Of-3+ wrapper class, but only once. And the rest of your classes will benefit from the Rule-Of-Zero.


The bullets:

  • One is able to peer and use the base type the #define (or typedef) HANDLE is built upon. For me this should be hidden knowledge, and a solution be based exclusively on what the interface makes available, HANDLE.

A: 90% of the time you can (and should) use std::unique_ptr<> with a custom deleter. The knowledge and responsibility for cleanup are still with the allocator. The way it should.

In the remaining cases, you'll have to write a single wrapper class for your specific resource that isn't otherwise supported.

  • Handles, can be anything, they're not required to be pointer types.

They can. You'd look at e.g. boost::optional. Or write that wrapper. The point is, you want it isolated for the resource. You don't want to complicate a class that happens to own/contain such a resource.

Citarella answered 14/2, 2013 at 0:58 Comment(5)
How does boost::optional help with those sorts of things? There's no way to use an integer with boost::optional.Austinaustina
@NicolBolas I have no idea what you mean. The point is should you want to manage lifetime on a value (non-pointer), boost::optional is there. In all other respects value objects are a moot point, because there are no ownership issues with values.Citarella
boost::optional can't manage the lifetime of handles. Handles do have ownership issues. There are APIs that aren't C++ APIs, you know.Austinaustina
I got the point of all this, but sadly, since the beginning, I thought it was clear none of this would answer the actual question, in EDIT 3 I show a trivial situation, and the former question is for an already existing tool (if there's one) that does this job properly, so I don't need to write it myself privately. Responding to write it myself completely dismiss the actual question.Kesler
@chico having a look at EDIT 3 nowCitarella
A
10

Is this handle focused RAII wrapper already done and usable in some cool library?

For your case, it's called unique_ptr. Observe:

struct WndLibDeleter
{
  typedef HANDLE pointer;

  void operator()(HANDLE h) {::FreeLibrary(h);}
};

using WndLibrary = std::unique_ptr<HANDLE, WndLibDeleter>;

WndLibrary LoadWndLibrary(std::wstring const& name)
{
  return WndLibrary(::LoadLibrary(name.c_str()));
}

With deleters, unique_ptr can service store and service any kind of object that is NullablePointer. HANDLE is a NullablePointer, so you can service it.

For objects that aren't NullablePointers, you'll have to use something else. The point of the Rule of Zero is to make "something else" as small as possible. It would be nothing more than a RAII-wrapper around the type, providing nothing more than access to it and move support. Therefore, the vast majority of your code does not need explicit copy/move constructors; just those few leaf classes.

Austinaustina answered 14/2, 2013 at 1:11 Comment(8)
+1 for the last paragraph. I've just added some more background myself :)Citarella
Thanks, the thing is, as this idiom through unique_ptr doesn't apply for some situations as I'd like, I already use the RAII-wrapper you talk about. As I don't like to think all programmers around the world doing their own minimal wrappers again and again, given the potential reusability of a proper handle RAII wrapper, I ask where it may be, if anyone implemented it. To use and endorse.Kesler
+1 for the non intrusive knowledge example.Kesler
Note that you can easily make any type a NullablePointer - the only requirements are that it's default constructible, you can assign nullptr to it and it is explicitly convertible to bool. Write a simple wrapper that fulfills those requirements. (boost::optional almost does, but it's not constructible from nullptr).Clarenceclarenceux
@Xeo: You can't do that for, for example, an integer, since you can't assign nullptr to an integer.Austinaustina
@Nicol: You can. Since nullptr represents the empty state, just write a wrapper that stores a boost::optional, and have a std::nullptr_t constructor that just initializes that member to the empty state.Clarenceclarenceux
@Xeo: So you're saying to take boost::optional and modify it to have a new constructor. Also, NullablePointer has more requirements than just setting equal to nullptr. You have to be able to compare it to nullptr. So there are more modifications to optional.Austinaustina
I never said to modify boost::optional. Just wrap it. Also, if you're constructible from nullptr, the comparision against that comes easily through implicit conversions - you only need to be comparable to yourself. Anyways, it's a very simple wrapper that can then adapt anything to the semantics of NullablePointer.Clarenceclarenceux

© 2022 - 2024 — McMap. All rights reserved.