Making COM nice to use
I write a lot of C++ code for Windows. There are a lot of Windows APIs which expose cool functionality and are implemented using COM; for me this currently means Direct2D, DirectWrite, Direct3D, DXGI, and Windows Imaging Component. In terms of presenting a compatible ABI, COM is a good thing (and is certainly nicer than straight flattening a C++ API to a C API). That said, COM looks extremely clunky in modern C++, for the following reasons:
- Exceptional behaviour is indicated as a non-successful
HRESULT
return value rather than by throwing an exception. In almost all cases, the correct thing to do upon seeing a non-successfulHRESULT
is to throw an exception, and so the default behaviour should be to throw an exception (rather than the current default of disregarding the return value). - As a result of return values being used for
HRESULT
s, actual return values end up becoming out-parameters. This precludes the use of method chaining, the use ofauto
in C++11, and other nice things. - As an extension of the previous point, there are cases where returning a
std::pair
or astd::tuple
would be preferable to the current situation of having multiple out-parameters. - As out-parameters are usually at the last of a method's parameters, the non-out-parameters cannot have default values.
- Interface pointers need to have
AddRef
andRelease
called at all appropriate points in the program, which is an additional burden on the programmer. With C++11's move semantics, there is almost no reason for manual reference counting. - There are often cases where ranges are passed as two parameters: a pointer (
void
or typed) along with a count (of either bytes or elements). In a lot of usage, a complete container gets passed, which should be accepted as-is. For example, givenstd::wstring str
, it should be allowable to sayobj->DrawText(str)
rather thanobj->DrawText(str.c_str(), str.size())
, and likewiseobj->DrawText(L"Message")
rather thanobj->DrawTect(L"Message", sizeof(L"Message") / sizeof(wchar_t) - 1)
. - As another case of coupled parameter pairs, a range of unrelated types will be handled as a least-common-denominator pointer and a
REFIID
. I should be able to writert->CreateSharedBitmap(surface)
rather than having to writert->CreateSharedBitmap(__uuidof(surface), surface)
. - Non-optional POD structures get passed by
const*
rather thanconst&
. My personal opinion is that in modern C++, if a pointer shouldn't ever benull
(i.e. is non-optional) and doesn't change what it points to (which for all intents and purposes, is true for method parameters), then it should always be a reference rather than a pointer. - As an elaboration on the previous point, a method which optionally accepts a POD structure should have two overloads (one without the parameter, and one with a
const&
) rather than accepting aconst*
and having a default value ofnull
. - Namespaces aren't used. For example, all the Direct2D interfaces are called
ID2D1Foo
rather thanD2::Foo
. - The default method naming convention is
CamelCase
, which looks slightly out of place alongside my default ofcamelCase
.
Some of those reasons are fairly minor, others are a major nuisance, but in aggregate, their overall effect makes COM programming rather unpleasant. Some people take small measures to attack one of the reasons individually, such as CHECK_HRESULT
macro for throwing an exception upon an unsuccessful HRESULT
(but you still need to wrap each call in this macro), or a com_ptr<T>
templated smart pointer which at least does AddRef
and Release
automatically (but you then need to unwrap the smart pointer when passing it as a parameter to a COM method). I think that a much better approach is to attack all of the problems simultaneously. It isn't as easy as writing a single macro or a single smart pointer template, but the outcome is nice-to-use COM rather than COM-with-one-less-problem.
As with switching on Lua strings, my answer is code generation. I have a tool which:
- Takes as input a set of COM headers (for example
d3d10.h
,d3d10_1.h
,d2d1.h
,dwrite.h
,dxgi.h
, andwincodec.h
). - Identifies every interface which is defined across this set of headers.
- For each interface, it writes out a new class (in an appropriate namespace) which is like a smart pointer on steroids:
- If the interface inherits from a base interface, then the smart class inherits from the smart class corresponding to the base interface.
- Just like a smart pointer,
AddRef
andRelease
are handled automatically by copy construction, move construction, copy assignment, and move assignment. - For each method of the interface, a new wrapper method (or set of overloaded methods) is written for the class:
- If the return type was
HRESULT
, then the wrapper checks for failure and throws an exception accordingly. - Out-parameters become return values (using a
std::tuple
if there was more than one out-parameter, or an out-parameter on a method which already had a non-void
and non-HRESULT
return type). - Coupled parameter pairs (such as pointer and length, or pointer and IID) become a single templated parameter.
- Pointers to POD structures get replaced with references to POD structures, with optional pointers becoming an overload which omits the parameter entirely.
- Pointers to COM objects get replaced with (references to) their corresponding smart class (in the case of in-parameters and also out-parameters).
- If the return type was
As an example, consider the following code which uses raw Direct2D to create a linear gradient brush:
// rt has type ID2D1RenderTarget*
// brush has type ID2D1LinearGradientBrush*
D2D1_GRADIENT_STOP stops[] = {
{0.f, colour_top},
{1.f, colour_bottom}};
ID2D1GradientStopCollection* stops_collection = nullptr;
HRESULT hr = rt->CreateGradientStopCollection(
stops,
sizeof(stops) / sizeof(D2D1_GRADIENT_STOP),
D2D1_GAMMA_2_2,
D2D1_EXTEND_MODE_CLAMP,
&stops_collection);
if(FAILED(hr))
throw Exception(hr, "ID2D1RenderTarget::CreateGradientStopCollection");
hr = rt->CreateLinearGradientBrush(
LinearGradientBrushProperties(Point2F(), Point2F())
BrushProperties(),
stops_collection,
&brush);
stops_collection->Release();
stops_collection = nullptr;
if(FAILED(hr))
throw Exception(hr, "ID2D1RenderTarget::CreateLinearGradientBrush");
With the nice-COM headers, the exact same behaviour is expressed in a much more concise manner:
// rt has type C6::D2::RenderTarget
// brush has type C6::D2::LinearGradientBrush
D2D1_GRADIENT_STOP stops[] = {
{0.f, colour_top},
{1.f, colour_bottom}};
brush = rt.createLinearGradientBrush(
LinearGradientBrushProperties(Point2F(), Point2F()),
BrushProperties(),
rt.createGradientStopCollection(
stops,
D2D1_GAMMA_2_2,
D2D1_EXTEND_MODE_CLAMP));