C++/WinRT: Understanding Weak References and the Dispose Pattern

Previous: Coroutines and the Thread Pool

I am going to interrupt the mini-series on coroutines to address a topic that came up on an internal discussion forum that may be helpful to a larger audience. Stay tuned for more on coroutines.

You might be surprised to learn that the Windows Runtime supports both weak references and the dispose pattern. At first glance, these do not seem like things that should belong in a platform built on COM and implemented in C++. This naturally leads to some confusion, so let me briefly explain how this came about.

The Component Object Model (COM) is based on a reference counting model for object lifetime and resource management in general. This is all rooted in the IUnknown interface and I describe all of this in detail in The Essentials of COM. Sometime later, Microsoft made the fateful decision (joking) to bet on garbage collection rather than reference counting for the Common Language Runtime, better known as the .NET Framework. As an aside, I must point out that the Windows Runtime is a far more effective “common language runtime” than the CLR ever was. 😊 Still, garbage collection has the nice property that it avoids reference cycles. There is no need for the notion of a weak reference in .NET because the garbage collector is quite capable of detecting cycles and handles them winsomely. Ironically, .NET ended up adding support for a form of weak references anyway. It is always amusing to see systems and languages inspired by C++ decide to design away some of its perceived problems only to realize that they are not so easily avoided. In practice reference cycles can usually be avoided quite easily with good API design. In other words, COM and WinRT can support well-designed APIs without introducing cyclic references or necessitating weak references.

Garbage collection and deterministic finalization are not at odds, but it is true that runtimes that rely on garbage collection tend to favor nondeterministic finalization. Consequently, developers often conflate the two. Brian Harry wrote a fantastic description of .NET’s use of nondeterministic finalization and I highly recommend you spend some time reading it. It is a very helpful history lesson, even though I might not agree with all the conclusions. The short of it is that .NET favors nondeterministic finalization but provides the explicit dispose pattern to support resource management. COM and thus WinRT relies on reference counting and thus inherently supports resource management without any need for something like the dispose pattern. Going further, C++/WinRT provides automatic reference counting, so you do not even have to think about the reference counting architecture and you end up with a very efficient deterministic programming model that is also very concise and less susceptible to resource management errors.

So why does the Windows Runtime offer both the IClosable and IWeakReferenceSource interfaces? Let me start by saying that while these two artifacts have a related etymology, they solve different problems and are essentially orthogonal.

IClosable exists to allow WinRT APIs to be implemented in C++ and consumed by languages that lack deterministic finalization. A C++ caller never needs to use IClosable, unless you are working around a bug, and C++/WinRT will never call it without you knowing it. The CLR will however present types that implement IClosable as if they implement the IDiposable interface so that C# callers can employ a using statement to gain deterministic resource management of WinRT types implemented in C++.

The notion of weak references, as implemented both by the C++ standard library and by the Windows Runtime, solves a different problem. Sometimes a developer might come to the realization that they simply cannot find a way to break a cyclic reference and said developer might reach for something like shared_ptr and its close friend the weak_ptr. As I said, cycles can often be avoided with good API design but sometimes you just don’t get a choice on that API design.

Of course, a good API design based on reference counting will likely look different than a good API design based on garbage collection. That’s what happened to the Windows Runtime. The first few versions of C++/WinRT lacked support for weak references. I didn’t seem to need an implementation, so I pressed on. When I started exploring XAML support I quickly realized that weak references are essential. XAML was not designed for COM and thus didn’t consider cycles an issue. When it came time to build a native implementation of XAML on the Windows Runtime, a mechanism was needed to handle cycles without fundamentally changing the way XAML works. That mechanism is the weak reference. Weak references are in fact insufficient to solve all of XAML’s reference tracking problems, but that’s a topic for another day.

Naturally, there’s nothing XAML-specific about weak references. A given design might necessitate weak references even if it has nothing to do with either XAML or .NET, hence the existence of shared_ptr and weak_ptr. I will however say that any C++ design that relies on shared_ptr or weak_ptr should think long and hard about whether what they have come up with is really the best design. Weak references and the dispose pattern are a reminder to me of our fallibility as human beings. They are concessions for an imperfect world.

That’s enough history and theory. How do they work? Let me start with IClosable. Consider a runtime class defined with modern IDL as follows:

namespace Component
{
    runtimeclass Class : Windows.Foundation.IClosable,
                         Windows.Foundation.IStringable
    {
        Class();
    }
}

I have thrown in IStringable just for illustration purposes. The cppwinrt -component option, and soon the new Visual Studio project templates, will get you started with the following C++ implementation class:

struct Class : ClassT<Class>
{
    Class() = default;

    void Close();
    hstring ToString();
};

I could have just crafted up an implementation directly, rather than relying on IDL or the cppwinrt -component option. Here’s essentially what this boils down to using only the C++/WinRT library:

struct Class : implements<Class, IClosable, IStringable>
{
    Class() = default;

    void Close();
    hstring ToString();
};

The first implements the entire WinRT class while the second merely implements the WinRT interfaces. Either way, the semantics and implementation of IClosable are the same. Now consider the following tracing implementation:

struct Class : ClassT<Class>
{
    Class()
    {
        puts("Class");
    }

    ~Class()
    {
        puts("~Class");
    }

    void Close()
    {
        puts("Close");
    }

    hstring ToString()
    {
        return L"Class";
    }
};

A C++ caller would look like this:

int main()
{
    Class c;                  // <-- prints "Class" here
    hstring s = c.ToString();
}                             // <-- prints "~Class" here

A C# caller would look like this:

void Main()
{
    using (var c = new Component.Class()) // <-- prints "Class" here
    {
        string s = c.ToString();
    }                                     // <-- prints "Close" here
}

As you can see, their behavior is entirely different. 😊 An implementation might call Close from its destructor, to provide uniform destruction, and ensure that Close is itself noexcept. You can read Richter’s dire warnings about implementing the dispose pattern correctly. So that’s the dispose pattern and as you can see, it has nothing to do with weak references. Speaking of which.

While the use of weak references should be rare, outside of XAML, it’s not immediately obvious if or when they will be needed on a given type. Certainly, it’s almost impossible for C++/WinRT to know without imposing some syntactic burden on the developer. Consequently, C++/WinRT provides weak reference support automatically unless you explicitly opt out. The implementation itself is pay-for-play, so this doesn’t cost you anything until someone queries for the IWeakReferenceSource interface. For example, here’s how I might get a weak reference to my class:

Class c;

weak_ref<Class> weak(c);

If you find typing the type name annoying, you can use the make_weak helper:

Class c;

auto weak = make_weak(c);

As you might expect, creating the weak reference does not affect the reference count on the object itself, but the act of requesting a weak reference will allocate a control block that takes care of implementing the weak reference semantics. Some time later, the caller may attempt to promote the weak reference to a strong reference:

auto weak = make_weak(c);

if (Class strong = weak.get())
{
    // success!
}

Provided some other strong reference still exists, the get call will increment the reference count and return it to the caller. While the implementation of IWeakReferenceSource is incomparably more complex that IClosable, a developer using C++/WinRT doesn’t have to think about that since C++/WinRT has implemented that once for all. So, avoid weak references if possible but if you really need them, by all means make use of this capability.

And that’s the not-so-short story of weak references and the dispose pattern in the Windows Runtime. Join me next time as I continue to explore C++/WinRT.