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-successful HRESULT 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
HRESULTs, actual return values end up becoming out-parameters. This precludes the use of method chaining, the use of auto in C++11, and other nice things.
- As an extension of the previous point, there are cases where returning a
std::pair or a std::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 and Release 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, given std::wstring str, it should be allowable to say obj->DrawText(str) rather than obj->DrawText(str.c_str(), str.size()), and likewise obj->DrawText(L"Message") rather than obj->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 write rt->CreateSharedBitmap(surface) rather than having to write rt->CreateSharedBitmap(__uuidof(surface), surface).
- Non-optional POD structures get passed by
const* rather than const&. My personal opinion is that in modern C++, if a pointer shouldn't ever be null (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 a const* and having a default value of null.
- Namespaces aren't used. For example, all the Direct2D interfaces are called
ID2D1Foo rather than D2::Foo.
- The default method naming convention is
CamelCase, which looks slightly out of place alongside my default of camelCase.
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, and wincodec.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 and Release 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).
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));
Comments
I like this post because it
I like this post because it is very very interesting.Thanks you very much for sharing this article.
Post new comment