Handles Comparison: empty classes vs. undefined classes vs. void*
Asked Answered
T

3

11

Microsoft's GDI+ defines many empty classes to be treated as handles internally. For example, (source GdiPlusGpStubs.h)

//Approach 1

class GpGraphics {};

class GpBrush {};
class GpTexture : public GpBrush {};
class GpSolidFill : public GpBrush {};
class GpLineGradient : public GpBrush {};
class GpPathGradient : public GpBrush {};
class GpHatch : public GpBrush {};

class GpPen {};
class GpCustomLineCap {};

There are other two ways to define handles. They're,

//Approach 2
class BOOK;  //no need to define it!
typedef BOOK *PBOOK;
typedef PBOOK HBOOK; //handle to be used internally

//Approach 3
typedef void* PVOID;
typedef PVOID HBOOK; //handle to be used internally

I just want to know the advantages and disadvantages of each of these approaches.

One advantage with Microsoft's approach is that, they can define type-safe hierarchy of handles using empty classes, which (I think) is not possible with the other two approaches, though I wonder what advantages this hierarchy would bring to the implementation? Anyway, what else?

EDIT:

One advantage with the second approach (i.e using incomplete classes) is that we can prevent clients from dereferencing the handles (that means, this approach appears to support encapsulation strongly, I suppose). The code would not even compile if one attempts to dereference handles. What else?

The same advantage one has with third approach as well, that you cannot dereference the handles.

Tamarind answered 24/12, 2010 at 10:47 Comment(2)
Related (not quite the same) question here: https://mcmap.net/q/130426/-the-right-type-for-handles-in-c-interfaces/50079. Unfortunately, there's not much to glean from it.Whang
I removed the C tag, as the question cannot pertain to C with option #1 on the table.Tortuous
S
3

Approach #1 is some mid-way between C style and C++ interface. Instead of member functions you have to pass the handle as argument. The advantage of exposed polymorphism is that you can reduce the amount of functions in interface and the types are checked compile time. Usually most experts prefer pimpl idiom (sometimes called compilation firewall) to such interface. You can not use approach #1 to interface with C so better go full C++.

Approach #2 is C style encapsulation and information hiding. The pointer may be (and often is) a pointer to real thing, so it is not over-engineered. User of library may not dereference that pointer. Disadvantage is that it does not expose any polymorphism. Advantage is that you may use it when interfacing with modules written in C.

Approach #3 is over-abstracted C-style encapsulation. The pointer may be really not a pointer at all since user of library should not cast, deallocate or dereference it. Advantage is that it may so carry exception or error values, disadvantage is that most of it has to be checked run time.

I agree with DeadMG that language-neutral object-oriented interfaces are very easy and elegant to use from C++, but these also involve more run-time checks than compile time checks and are overkill when i don't need to interface with other languages. So i personally prefer Approach #2 if it needs to interface with C or Pimpl idiom when it is C++ only.

Steady answered 24/12, 2010 at 18:43 Comment(3)
@Öö Tiib : this is very good post. could you please elaborate the lines where you've used the phrase 'polymorphism'. Especially how does approach#1 exposes polymorphism while approach#2 doesn't?Tamarind
@Nawaz : #1 exposes that GpTexture is a GpBrush so pointer to GpTexture can be used as pointer to GpBrush. #2 exposes nothing about forward-declared Book. There may be polymorphism in Book's implementation, but it is hidden and not exposed by interface.Spy
@Öö Tiib : thanks for that little elaboration. I was thinking the same. Now,I'm accepting your answer :-)Tamarind
T
2

Approach 3 is not very good at all, as it allows the mixing and matching of handle types that don't actually make sense, any function that takes a HANDLE can take any HANDLE, even if it's compile-time determinable that that is the wrong type.

The downside of Approach 1 is that you have to do a bunch of casting on the other end to their actual types.

Approach 2 isn't that bad, except you can't do any kind of inheritance with it without having to externally query every time.

However, all of this is entirely moot ever since compilers discovered how to implement efficient virtual functions. The approach taken by DirectX and COM is the best- it's very flexible, powerful, and completely type-safe.

It even allows for some truly insane things, like you can inherit from DirectX interfaces and extend it that way. One of the best advantages of this is Direct2D and Direct3D11. They're not actually compatible (which is truly, horrendously stupid), but you can define a proxy type that inherits from ID3D10Device1 and forwards to the ID3D11Device and solve the problem like that. That kind of thing would never even think about being possible with any of the above approaches.

Oh, and last thing: You really, really shouldn't name your types in allcaps.

Tortuous answered 24/12, 2010 at 12:54 Comment(3)
instead of "bunch of casting" one can use another type hierarchy (with real pointers or references) for implementationZelazny
I didn't understand this "The downside of Approach 1 is that you have to do a bunch of casting on the other end to their actual types.". Why?Tamarind
@Nawaz: Because the classes are defined as empty. When you write the implementation code and you include the header, you're defining functions that take pointers to a bunch of empty classes, which are obviously quite useless for implementing anything, necessitating a cast to the real implementation type.Tortuous
Z
1

2 and 3 are slightly less typesafe as they allow to use handles instead of void*

void bluescreeen(HBOOK hb){
  memset(hb,0,100000); // no compile errors
}
Zelazny answered 24/12, 2010 at 12:37 Comment(3)
That's not valid C++, as a BOOK* cannot be implicitly converted to a void*.Tortuous
@DeadMG any pointer may be implicitly converted to void*, both in C and C++. Only reverse conversion (from void* to ptr*) is prohibited in C++Zelazny
@user39662 : how approach 2 is less typesafe while 1 is not?Tamarind

© 2022 - 2024 — McMap. All rights reserved.