Is relying on the type of a Windows handle being a pointer ok?
Asked Answered
U

4

3

Windows handles are sometimes annoying to remember to clean up after (doing GDI with created pens and brushes is a great example). An RAII solution is great, but is it really that great making one full (Rule of Five) RAII class for each different type of handle? Of course not! The best I can see would be one full generic RAII class with other classes just defining what to do when the handle should be cleaned up, as well as other handle-specific aspects.

For example, a very simple module class could be defined like this (just an example):

struct Module {
    Module() : handle_{nullptr} {}
    Module(HMODULE hm) : handle_{hm, [](HMODULE h){FreeLibrary(h);}} {}
    operator HMODULE() const {return handle_.get();}

private:
    Handle<HMODULE> handle_;
};

That's all fine and dandy, and no destructor or anything is needed. Of course, though, being able to write the Handle class to not need a destructor or anything as well would be nice, too. Why not use existing RAII techniques? One idea would be to use a smart pointer to a void, but that won't work. Here's how the handles are actually declared under normal circumstances:

#define DECLARE_HANDLE(n) typedef struct n##__{int i;}*n
DECLARE_HANDLE(HACCEL);
DECLARE_HANDLE(HBITMAP);
DECLARE_HANDLE(HBRUSH);
...

It actually differentiates between handle types, which is good, but it makes using a smart pointer to void impossible. What if, instead, since handles are, by definitions, pointers, the type could be extracted?

My question is whether the following is a safe assumption to make. It uses a handle to a desktop, which must be closed. Barring the differences between shared and unique pointers (e.g., FreeLibrary has its own reference counting semantics), is assuming the handle is a pointer and making a smart pointer to whatever it's pointing to okay, or should I not use smart pointers and make Handle implement the RAII aspects itself?

#include <memory>
#include <type_traits>
#include <utility>
#include <windows.h>

int main() {
    using underlying_type = std::common_type<decltype(*std::declval<HDESK>())>::type;
    std::shared_ptr<underlying_type> ptr{nullptr, [](HDESK desk){CloseDesktop(desk);}};
}
Urbanize answered 21/5, 2013 at 18:11 Comment(3)
IMO, for what's currently available in C++11 standard library, it's not: https://mcmap.net/q/740440/-is-there-a-proper-39-ownership-in-a-package-39-for-39-handles-39-availableChoplogic
That's why such proposals exist: andrewlsandoval.com/scope_exitChoplogic
@chico, Interesting, I didn't catch that one when looking.Urbanize
J
2

I believe all Windows pointers are technically pointers to internal objects inside the Windows kernel part of the system (or sometimes, possibly, to user-side objects allocated by the kernel code, or some variation on that theme).

I'm far from convinced that you should TREAT them as pointers tho'. They are only pointers in a purely technical perspective. They are no more "pointers" than C style "FILE *" is a pointer. I don't think you would suggest the use of shared_ptr<FILE*> to deal with closing files later on.

Wrapping a handle into something that cleans it up later is by all means a good idea, but I don't think using smart pointer solutions is the right solution. Using a templated system which knows how to close the handle would be the ideal.

I suppose you also would need to deal with "I want to pass this handle from here to somewhere else" in some good way that works for all involved - e.g. you have a function that fetches resources in some way, and it returns handles to those resources - do you return an already wrapped object, and if so, how does the copy work?

What if you need to save a copy of a handle before using another one (e.g. save current pen, then set a custom one, then restore)?

Jedidiah answered 21/5, 2013 at 22:38 Comment(10)
So are you advocating that I use the single full RAII class option, which would create semantics similar to that of a smart pointer (tweaked to be more handle-oriented) without actually needing to know any implementation details? I have also thought a bit about assuming ownership vs. not assuming ownership, and while I haven't thought of a perfect solution, I'm going to keep that in mind as I plan out exactly how I want things to work. I tried not to let that get in the way of this question, but it's a very important aspect to plan for.Urbanize
The "not getting in the way of the question" is probably a poor idea. Understand how things are to be used, then build a system that does what you need. I'm not sure my answer is as much of an answer as a bit of discussion on the subject - I'm absolutely sure I don't know ALL the tricky bits you need to deal with in this case, I'm just throwing a few bits into the mix for you to think about... But I would personally not like a shared_ptr<FILE *> style solution, and I don't think it works well for shared_ptr<HWND> either...Jedidiah
I just didn't see those considerations as being all that relevant. They can be built in the same way regardless of whether I use a smart pointer, follow the Rule of Five myself, or do away with the base Handle helper class altogether. The main reason I haven't sat down and thought about that part all too much is that I'm getting this part of it out of the way first with a general idea of what I hope it to look like finished. Anyway, I believe you meant shared_ptr<HWND__> or shared_ptr<FILE>, since the smart pointer would replace the actual one and pass a PointedTo * to the deleter.Urbanize
So do you think just changing the Handle class to be implemented better than with smart pointers is good enough for handling cleanup (i.e., my Module etc. classes could be written along those lines without extra cleanup code), or do you think I need to not use a cleanup helper class at all and do it specifically in each Module-like class?Urbanize
Not sure what I think - I'm kind of old fashioned and think that programmers can actually think [and write good code] most of the time. But then I do see some examples on here proving otherwise, although not entirely sure that any form of clever classes would rescue those projects... ;) [no, that's not a comment on this particular question at all, and not being sarcastic] What happens "behind the scenes" is kind of irrelevant. Providing a neat and clean interface is a more important factor, that will make the class more likely to be used by others than the creator...Jedidiah
Fair enough. I know exactly what you mean by programmers being smart and by examples sometimes not being smart ( :( ). My aim when I do this is generally to make it easiest to use the classes and then as easy as I can to implement them without duplicating code, and duplicating the cleanup of handles is a lot of code and is also annoying to do when using the resources (you don't want to know how many GDI resources I've caught myself leaking in small programs where I do GDI because I don't have to look anything up).Urbanize
@MatsPetersson: Handles are only technically pointers insofar as they're void*. In real, they're more something like ptrdiff_t -- offsets into a table of pointers. You can see that by looking at their values. Handles that you open in your application are usually "small" numbers (starting at 16 or 24), this can be explained by 3-4 handles already being open for stdio, stdout, the exe image mapping and such, and their values are always multiples of 4 (8 on 64 bits). So that looks just like an offset into a table of 4-byte (8-byte) values -- pointers to the actual internal structures.Skull
@Damon: I'm pretty certain that at least SOME handles are NOT small numbers - as I have observed them. I think it may differ depending on what the actual handle represents (and it may also be different between Win32 code on a 64-bit OS ant native 32/64-bit code). I'm absolutely sure that some handles are direct pointers to OS structures. I just started up my laptop, and on that the HWND and HINSTANCE of an existing project that I worked on a few years ago are definitely pointers that the debugger can look into (they have only one field that is called "unused").Jedidiah
@MatsPetersson: Ah yes, HINSTANCE is a special case, the one you get in WinMain is (or at least used to be, unless they changed it...) the base address of the main executable, or 0x4000000 (if built with default linker options). But I think this one counts as "pseudo" handle much like (HANDLE)-1 which is what functions like GetCurrentProcess return -- either way, I only wanted to point out that I agree with you insofar as even if handles are technically pointers, they really aren't, and treating them as such makes me feel uneasy.Skull
@Damon: HWND is definitely not pointing to my code - and I don't think my base-address is whatever HINSTANCE contains (I've shut down my computer, and it takes ages to boot because something isn't quite right with it [or it's busying itself with other things than what I ask for, because I haven't been using it regularly, possibly - computers don't like being neglected, I've noticed]. But yes, I agree, you should treat them more like "some sort of reference to a resource that I need to close/free later" than truly as pointers - just like a FILE * isn't a pointer...Jedidiah
R
2

One approach you could take is to use a template class:

template<typename H, BOOL(WINAPI *Releaser)(H)>
class Handle
{
private:
    H m_handle;

public:
    Handle(H handle) : m_handle(handle) { }
    ~Handle() { (*Releaser)(m_handle); }
};

typedef Handle<HANDLE,&::CloseHandle> RAIIHANDLE;
typedef Handle<HMODULE,&::FreeLibrary> RAIIHMODULE;
typedef Handle<HDESK,&::CloseDesktop> RAIIHDESKTOP;

If there is a HANDLE which isn't released by a function of type BOOL(WINAPI)(HANDLE), then you may have issues with this. If the releasing functions only differ by return type though, you could add that as a template parameter and still use this solution.

Responsibility answered 21/5, 2013 at 19:16 Comment(3)
I've already implemented something of the sort using smart pointers to play around with, though it takes a void(*)(HandleType) and leaves error checking on close to the user, which would best be internally by a class like Module or what have you. I was more wondering whether it's a solid idea to have my Handle class not have to implement a destructor and all that by relying on a handle type being a pointer.Urbanize
I think Damon sums up well why using a void* should work in theory, and why it's perhaps not a good idea to do it. All I can add is that with my solution you do not have to specify the releasing function every time you instantiate an RAII handle wrapper (if I understand your current solution correctly). This will probably make your code less verbose and clearer. You could possibly also implement some basic error checking for invalid values in the template class by passing another template parameter of type H, e.g. typedef Handle<HANDLE,&::CloseHandle,INVALID_HANDLE_VALUE> FileHandle.Responsibility
You're right in that it would make the code less verbose, but I feel those typedefs wouldn't always work that great for everything. Better to replace the typedefs with custom classes that can have more possibilities related to the handle, and make it work more in the sense of "I'd like a class to represent functionality of this handle, but I'd like the cleanup to be done for me."Urbanize
J
2

I believe all Windows pointers are technically pointers to internal objects inside the Windows kernel part of the system (or sometimes, possibly, to user-side objects allocated by the kernel code, or some variation on that theme).

I'm far from convinced that you should TREAT them as pointers tho'. They are only pointers in a purely technical perspective. They are no more "pointers" than C style "FILE *" is a pointer. I don't think you would suggest the use of shared_ptr<FILE*> to deal with closing files later on.

Wrapping a handle into something that cleans it up later is by all means a good idea, but I don't think using smart pointer solutions is the right solution. Using a templated system which knows how to close the handle would be the ideal.

I suppose you also would need to deal with "I want to pass this handle from here to somewhere else" in some good way that works for all involved - e.g. you have a function that fetches resources in some way, and it returns handles to those resources - do you return an already wrapped object, and if so, how does the copy work?

What if you need to save a copy of a handle before using another one (e.g. save current pen, then set a custom one, then restore)?

Jedidiah answered 21/5, 2013 at 22:38 Comment(10)
So are you advocating that I use the single full RAII class option, which would create semantics similar to that of a smart pointer (tweaked to be more handle-oriented) without actually needing to know any implementation details? I have also thought a bit about assuming ownership vs. not assuming ownership, and while I haven't thought of a perfect solution, I'm going to keep that in mind as I plan out exactly how I want things to work. I tried not to let that get in the way of this question, but it's a very important aspect to plan for.Urbanize
The "not getting in the way of the question" is probably a poor idea. Understand how things are to be used, then build a system that does what you need. I'm not sure my answer is as much of an answer as a bit of discussion on the subject - I'm absolutely sure I don't know ALL the tricky bits you need to deal with in this case, I'm just throwing a few bits into the mix for you to think about... But I would personally not like a shared_ptr<FILE *> style solution, and I don't think it works well for shared_ptr<HWND> either...Jedidiah
I just didn't see those considerations as being all that relevant. They can be built in the same way regardless of whether I use a smart pointer, follow the Rule of Five myself, or do away with the base Handle helper class altogether. The main reason I haven't sat down and thought about that part all too much is that I'm getting this part of it out of the way first with a general idea of what I hope it to look like finished. Anyway, I believe you meant shared_ptr<HWND__> or shared_ptr<FILE>, since the smart pointer would replace the actual one and pass a PointedTo * to the deleter.Urbanize
So do you think just changing the Handle class to be implemented better than with smart pointers is good enough for handling cleanup (i.e., my Module etc. classes could be written along those lines without extra cleanup code), or do you think I need to not use a cleanup helper class at all and do it specifically in each Module-like class?Urbanize
Not sure what I think - I'm kind of old fashioned and think that programmers can actually think [and write good code] most of the time. But then I do see some examples on here proving otherwise, although not entirely sure that any form of clever classes would rescue those projects... ;) [no, that's not a comment on this particular question at all, and not being sarcastic] What happens "behind the scenes" is kind of irrelevant. Providing a neat and clean interface is a more important factor, that will make the class more likely to be used by others than the creator...Jedidiah
Fair enough. I know exactly what you mean by programmers being smart and by examples sometimes not being smart ( :( ). My aim when I do this is generally to make it easiest to use the classes and then as easy as I can to implement them without duplicating code, and duplicating the cleanup of handles is a lot of code and is also annoying to do when using the resources (you don't want to know how many GDI resources I've caught myself leaking in small programs where I do GDI because I don't have to look anything up).Urbanize
@MatsPetersson: Handles are only technically pointers insofar as they're void*. In real, they're more something like ptrdiff_t -- offsets into a table of pointers. You can see that by looking at their values. Handles that you open in your application are usually "small" numbers (starting at 16 or 24), this can be explained by 3-4 handles already being open for stdio, stdout, the exe image mapping and such, and their values are always multiples of 4 (8 on 64 bits). So that looks just like an offset into a table of 4-byte (8-byte) values -- pointers to the actual internal structures.Skull
@Damon: I'm pretty certain that at least SOME handles are NOT small numbers - as I have observed them. I think it may differ depending on what the actual handle represents (and it may also be different between Win32 code on a 64-bit OS ant native 32/64-bit code). I'm absolutely sure that some handles are direct pointers to OS structures. I just started up my laptop, and on that the HWND and HINSTANCE of an existing project that I worked on a few years ago are definitely pointers that the debugger can look into (they have only one field that is called "unused").Jedidiah
@MatsPetersson: Ah yes, HINSTANCE is a special case, the one you get in WinMain is (or at least used to be, unless they changed it...) the base address of the main executable, or 0x4000000 (if built with default linker options). But I think this one counts as "pseudo" handle much like (HANDLE)-1 which is what functions like GetCurrentProcess return -- either way, I only wanted to point out that I agree with you insofar as even if handles are technically pointers, they really aren't, and treating them as such makes me feel uneasy.Skull
@Damon: HWND is definitely not pointing to my code - and I don't think my base-address is whatever HINSTANCE contains (I've shut down my computer, and it takes ages to boot because something isn't quite right with it [or it's busying itself with other things than what I ask for, because I haven't been using it regularly, possibly - computers don't like being neglected, I've noticed]. But yes, I agree, you should treat them more like "some sort of reference to a resource that I need to close/free later" than truly as pointers - just like a FILE * isn't a pointer...Jedidiah
S
1

Technically, this ought to work perfectly well under all present versions of Windows, and it is hard to find a real reason against doing it (it is actually a very clever use of existing standard library functionality!), but I still don't like the idea because:

  1. Handles are pointers, and Handles are not pointers. Handles are opaque types, and one should treat them as such for compatibility. Handles are pointers, and it is unlikely that handles will ever be something different, but it is possible that this will change. Also, handles may or may not have values that are valid pointer values, and they may or may not have different values under 32bit and 64bit (say INVALID_HANDLE_VALUE), or other side effects or behaviours that you maybe don't foresee now. Assuming that a handle has certain given properties may work fine for decades, but it may (in theory) mysteriously fail in some condition that you didn't think about. Admittedly, it is very unlikely to happen, but still it isn't 100% clean.
  2. Using a smart pointer in this way doesn't follow the principle of least astonishment. Because, hey, handles aren't pointers. Having RAII built into a class that is named with an intuitive name ("Handle", "AutoHandle") will not cause anyone to raise an eyebrow.
Skull answered 21/5, 2013 at 19:2 Comment(1)
Interesting analysis. About number 1, its only purpose is for automatic cleanup that is specified at the time of creation. It wouldn't concern itself with whether the handle has a valid value, so INVALID_HANDLE_VALUE would still have to be checked on a per-handle basis when applicable. The possibility of them becoming not a pointer seems unlikely (C compatibility and being passed into a void * without casting). I do fully agree that there's something I missed now that could hurt later, which is pretty annoying to have floating around in my head. My aim is to only assume they're pointers.Urbanize
C
1

I believe that both unique_ptr and shared_ptr allow you to provide custom deleter. Which I believe is just what you need to correctly manage lifetime of handles.

Cavite answered 21/5, 2013 at 20:28 Comment(2)
Yes, and those are what I used in the test Handle class I made to experiment this idea with, so I know they work as long as you keep things like internal reference counting in mind. I was asking if such a handle class that used smart pointers was a good idea or if it would screw me over later. I've had a number of experiences where utilities I made were too broad, so there were all sorts of cases they wouldn't work with, or just plain stupid.Urbanize
Both unique_ptr and shared_ptr are named in a way which suggests they are just better pointers. I don't agree with this and I just see them as RAII clases and the fact they do provide customer deleter is an argument to support this opinion. I would say - go ahead, use it.Cavite

© 2022 - 2024 — McMap. All rights reserved.