C++/CX Part 2 of [n]: Types That Wear Hats


Visual C++

See C++/CX Part 0 of [N]: An Introduction for an introduction to this series.

The hat (^) is one of the most prominent features of C++/CX–it’s hard not to notice it when one first sees C++/CX code. So, what exactly is a ^ type? A hat type is a smart pointer type that (1) automatically manages the lifetime of a Windows Runtime object and (2) provides automatic type conversion capabilities to simplify use of Windows Runtime objects.

We’ll start off by discussing how Windows Runtime objects are used via WRL, then explain how the C++/CX hat works to make things simpler. For demonstration purposes, we’ll use the following modified version of the Number class that we introduced in Part 1:

In this modified Number implementation, we define a pair of interfaces, IGetValue and ISetValue, that declare the two member functions of Number; Number then implements these two interfaces. Otherwise, things should look pretty familiar.

Note that Number actually implements three Windows Runtime interfaces: in addition to IGetValue and ISetValue, the compiler still generates the __INumberPublicNonVirtuals interface, which Number implements. Because all of the members of Number are declared by explicitly implemented interfaces (IGetValue and ISetValue), the compiler-generated __INumberPublicNonVirtuals does not declare any members. This interface is still required, though, as it is the default interface for the Number type. Every runtime type must have one default interface, and the default interface should almost always be unique to the class. We’ll see why the default interface is important a bit later.

Lifetime Management

Windows Runtime reference types use reference counting for object lifetime management. All Windows Runtime interfaces (including all three of the interfaces implemented by Number) derive directly from the IInspectable interface, which itself derives from the COM IUnknown interface. IUnknown declares three member functions that are used to control the lifetime of an object and to allow type conversion.

The MSDN article “Rules for Managing Reference Counts” has a thorough overview of how IUnknown lifetime management works. The principles are quite straightforward, though: whenever you create a new reference to an object, you must call IUnknown::AddRef to increment its reference count; whenever you “destroy” a reference to an object, you must call IUnknown::Release to decrement the reference count. The reference count is initialized to zero, and after a series of calls to AddRef and Release, when the reference count reaches zero again, the object destroys itself.

Of course, when programming in C++, we should rarely–practically never–call AddRef and Release directly. Instead, we should prefer wherever possible to use a smart pointer that automatically makes these calls when they are required. Use of a smart pointer helps to ensure that objects are neither leaked due to a missed Release nor prematurely destroyed due to a premature Release or failure to AddRef.

ATL includes CComPtr and a family of related smart pointers that have long been used in COM programming for automatically managing the reference counting of objects that implement IUnknown. WRL includes ComPtr, which is an improved and modernized CComPtr (an example improvement: ComPtr does not overload the unary & like CComPtr does).

For those who have not done much COM programming and are unfamiliar with ComPtrs: if you’ve used shared_ptr (included as part of C++11, C++ TR1, and Boost), ComPtr has effectively the same behavior with respect to lifetime management. The mechanism is different (ComPtr makes use of the internal reference counting provided by IUnknown, while shared_ptr supports arbitrary types and must thus use an external reference count), but the lifetime management behavior is the same.

The C++/CX hat has exactly the same lifetime management semantics as a ComPtr. When a T^ is copied, AddRef is called to increment the reference count, and when a T^ goes out of scope or is reassigned, Release is called to decrement the reference count. We can consider a simple example to demonstrate the reference counting behavior:

First, we create an A object and give ownership of it to t0. The reference count of this A object is 1 because there is one T^ that has a reference to it. We then create a B object and give ownership of it to t1. The reference count of this B object is also 1.

The end result of t0 = t1 is that both t0 and t1 point to the same object. This must be done in three steps. First, t1->AddRef() is called to increment the reference count of the B object, because t1 is gaining ownership of the object. Second, t0->Release() is called to release t0‘s ownership of the A object. This causes the reference count of the A object to drop to zero and the A object destroys itself. This causes the reference count of the B object to increase to 2. Third and finally, t1 is set to point to the B object.

We then assign t0 = nullptr. This “resets” t0 to be null, which causes it to release its ownership of the B object. This calls t0->Release(), causing the reference count of the B object to decrease to 1.

Finally, execution will reach the closing brace of the block: }. At this point, all of the local variables are destroyed, in reverse order. First, t1 is destroyed (the smart pointer, not the object pointed to). This calls t1->Release(), causing the reference count of the B object to drop to zero, so the B object destroys itself. t0 is then destroyed, which is a no-op because it is null.

If lifetime management was our only concern, there wouldn’t really be a need for the ^ at all: ComPtr<T> is sufficient to manage object lifetime.

Type Conversion

In C++, some type conversions involving class types are implicit; others may be performed using a cast or a series of casts. For example, if Number and the interfaces it implements were ordinary C++ types and not Windows Runtime types, conversion from Number* to IGetValue* would be implicit, and we could convert from IGetValue* to Number* using a static_cast or a dynamic_cast.

These conversions do not work with Windows Runtime reference types because the implementation of a reference type is opaque and the layout of a reference type in memory is left unspecified. A reference type implemented in C# may be laid out in memory differently from an equivalent reference type implemented in C++. Thus, we cannot rely on C++ language specific features like implicit derived-to-base conversions and casts when working directly with Windows Runtime types.

To perform these conversions, we must instead use the third member function of the IUnknown interface: IUnknown::QueryInterface. This member function can be considered as a language-neutral dynamic_cast: it attempts to perform a conversion to the specified interface and returns whether the conversion succeeded. Because each runtime type implements the IUnknown interface and provides its own definition for QueryInterface, it can do whatever is required to get the correct interface pointer in the language and framework with which it is implemented.

Using an object via WRL

Let’s take a look at how we’d use our Number class via WRL. This example accepts an instance of Number and calls SetValue to set the value and GetValue to get the value back. (Error checking has been omitted for brevity.)

The As member function template of the WRL ComPtr simply encapsulates a call to IUnknown::QueryInterface in a way that helps to prevent common programming errors. We use this first to get the ISetValue interface pointer to call SetValue then again to get the IGetValue interface pointer to call GetValue.

This would be a lot simpler if we obtained a Number* and called SetValue and GetValue via that pointer. Unfortunately, we can’t do that: recall that the implementation of a reference type is opaque and that we only ever interact with an object via a pointer to one of the interfaces that it implements. This means that we can never have a Number*; there is really no such thing. Rather, we can only refer to a Number object via an IGetValue*, an ISetValue*, or an __INumberPublicNonVirtuals*.

This is an awful lot of code just to call two member functions and this example demonstrates one of the key hurdles that we have to overcome to make it easier to use Windows Runtime types. Unlike COM, Windows Runtime does not allow an interface to derive from another Windows Runtime interface; all interfaces must derive directly from IInspectable. Each interface is independent, and we can only interact with an object via interfaces, so if we are using a type that implements multiple interfaces (which many types do), we are stuck having to write a lot of rather verbose type conversion code so that we can obtain the correct interface pointer to make each function call.

Using an object via C++/CX

One of the key advantages of C++/CX is that the compiler knows which types are Windows Runtime types. It has access to the Windows Metadata (WinMD) file that defines each interface and runtime type, so–among other things–it knows the set of interfaces that each runtime type implements. For instance, the compiler knows that the Number type implements the ISetValue and IGetValue interfaces, because the metadata specifies that it does. The compiler is able to use this type information to automatically generate type conversion code.

Consider the following C++/CX example, which is equivalent to the WRL example that we presented:

Because the compiler knows that the Number type implements the ISetValue and IGetValue interfaces, it allows implicit conversion from Number^ to ISetValue^ and IGetValue^. This implicit conversion causes the compiler to generate a call to IUnknown::QueryInterface to get the right interface pointer. Aside from the cleaner syntax, there’s really no magic here: the compiler is just generating the type conversion code that we otherwise would have to write ourselves.

dynamic_cast also works much as we’d expect: we can, for example, amend this example to obtain the IGetValue^ from the ISetValue^:

This example has the same behavior as the first example, we just take different steps to get that same behavior. dynamic_cast can return nullptr if the cast fails (though we know that it will succeed in this specific case). C++/CX also provides safe_cast, which throws a Platform::InvalidCastException exception if the cast fails.

When we discussed the WRL example above, we noted that there’s no such thing as a Number*: we only ever work with interface pointers. This begs the question: what is a Number^? At runtime, a Number^ is a __INumberPublicNonVirtuals^. A hat that refers to a runtime type (and not an interface) actually holds a pointer to the default interface of that runtime type.

At compile-time, though, the compiler treats Number^ as if it refers to the whole Number object. The compiler aggregates all of the members of all of the interfaces implemented by Number and allows all of those members to be called directly via a Number^. We can use a Number^ as if it were an IGetValue^ or an ISetValue^ and the compiler will inject the required calls to QueryInterface to perform the conversions required to make the function call.

Therefore, we can shorten our C++/CX program further:

This code does exactly the same thing as our first C++/CX example and as our WRL example. There’s still no magic: the compiler is simply generating all of the boilerplate to perform the type conversion required to make each function call.

You may have noticed that this example is much shorter and less verbose than the WRL example that we started with. 🙂 All of the boilerplate is gone and we’re left with code that–aside from the ^ and ref that tell the compiler we are dealing with Windows Runtime types–looks exactly like similar C++ code that interacts with ordinary C++ types. This is the point, though: ideally our code that uses Windows Runtime types should be as similar as possible to code that uses C++ types.

Final Notes

Both ComPtr<T> and T^ are “no-overhead” smart pointers: the size of each is the same as the size of an ordinary pointer, and operations using them do not do any unnecessary work. If you need to interoperate between code that uses C++/CX and code that uses WRL, you can simply use reinterpret_cast to convert a T^ to a T*:

(The ABI level definitions of types are defined in namespaces under the ABI namespace so that they do not conflict with the “high-level” definitions of the types used by C++/CX, which are defined under the global namespace.)

In addition to its type conversion capabilities, the hat provides other benefits that could not otherwise be accomplished via use of an ordinary smart pointer like ComPtr. One of the most important of these benefits is that the hat can be used uniformly, everywhere. A member function that takes an interface pointer as an argument is declared as taking a raw pointer (this is part of the Windows Runtime ABI, which is designed to be simple and language-neutral, and thus knows nothing of what a C++ smart pointer is). So, while one can use ComPtr most places, raw pointers still need to be used at the ABI boundary, and there is room for subtle (and not-so-subtle) programming errors.

With C++/CX, the compiler already transforms member function signatures to translate between exceptions and HRESULTs and the compiler is also able to inject conversions from T^ to T* where required, substantially reducing the opportunity for programming errors.

Visual C++

Follow Visual C++   

No Comments.