June 2nd, 2023

C++/WinRT event handlers that are lambdas with weak pointers to the parent class, part 3

Last time, we created a helper function for creating a delegate from a weak pointer and a lambda. The weak pointer in question was a C++/WinRT weak pointer, but maybe you want to use this from a traditional C++ class, in which case what you have is not a winrt::weak_ref but rather a std::weak_ptr. Let’s adapt our function to work with std::weak_ptr too.

Recall that we left off last time with this function:

template<typename T, typename Handler>
auto weak_delegate(
    winrt::weak_ref<T> weak,
    Handler&& handler)
{
  return [weak = std::move(weak),
          handler = std::forward<Handler>(handler)]
    (auto&&... args) mutable
    {
      if (auto strong = weak.get()) {
        handler(std::forward<decltype(args)>(args)...);
      }
    };
}

The only difference that matters (for our purposes) between winrt::weak_ref and std::weak_ptr is that winrt::weak_ref uses the get() method to promote the weak reference to a strong reference, whereas the corresponding method on std::weak_ptr is called lock().

What we want to do is something weird like

template<typename Weak, typename Handler>
auto weak_delegate(
    Weak&& weak,
    Handler&& handler)
{
  return [weak = std::forward<Weak>(weak),
          handler = std::forward<Handler>(handler)]
    (auto&&... args) mutable
    {
      if constexpr (is_specialization_v<Weak, std::weak_ptr>) {
        auto strong = weak.lock();
      } else {
        auto strong = weak.get();
      }
      if (strong) {
        handler(std::forward<decltype(args)>(args)...);
      }
    };
}

But that doesn’t work because the scope of strong is wrong.¹

What we could do is turn it into a lambda:

template<typename Weak, typename Handler>
auto weak_delegate(
    Weak&& weak,
    Handler&& handler)
{
  return [weak = std::forward<Weak>(weak),
          handler = std::forward<Handler>(handler)]
    (auto&&... args) mutable
    {
      auto strong = [&] {
        if constexpr (is_specialization_v<Weak, std::weak_ptr>) {
          return weak.lock();
        } else {
          return weak.get();
        }
      }();
      if (strong) {
        handler(std::forward<decltype(args)>(args)...);
      }
    };
}

Now, writing that is_specialization_v thingie is going to be a bit of an annoyance, but it turns out we don’t need to do it. We can let function overloading do the work.

template<typename... Extra>
auto try_resolve_weak_pointer_to_strong(std::weak_ptr<Extra...> const& weak)
{
    return weak.lock();
}

template<typename... Extra>
auto try_resolve_weak_pointer_to_strong(winrt::weak_ref<Extra...> const& weak)
{
    return weak.get();
}

template<typename Weak, typename Handler>
auto weak_delegate(
    Weak&& weak,
    Handler&& handler)
{
  return [weak = std::forward<Weak>(weak),
          handler = std::forward<Handler>(handler)]
    (auto&&... args) mutable
    {
      if (auto strong = try_resolve_weak_pointer_to_strong(weak)) {
        handler(std::forward<decltype(args)>(args)...);
      }
    };
}

The try_resolve_weak_pointer_to_strong functions use a trick we looked at a little while ago.

Now you can use weak_delegate with C++ standard library weak pointers, as well as C++/WinRT weak references.

struct MyClass : std::enable_shared_from_this<MyClass>
{
    ...

    void RegisterSomething(int otherData)
    {
      widget.Something(weak_delegate(weak_from_this(),
        [this, otherData]
        (auto&& sender, auto&& args)
        {
          DoThing1(sender);
          DoThing2(args);
          DoThing3(otherData);
        }));
    }
};

A benefit of delegating the “resolve a weak pointer to a strong pointer” decision to a function overload is that you can extend support to other weak pointer libraries by adding new overloads.

// WRL WeakRef
auto try_resolve_weak_pointer_to_strong(::Microsoft::WRL::WeakRef const& weak)
{
    ::Microsoft::WRL::ComPtr<IInspectable> o;
    weak.As(&o);
    return o;
}

// wil com_weak_ptr
template<typename... Extra>
auto try_resolve_weak_pointer_to_strong(
    wil::com_ptr<IWeakReference, Extra...> const& weak)
{
    return weak.try_copy<IUnknown>();
}

// Unreal Engine TWeakPtr
template<typename... Extra>
auto try_resolve_weak_pointer_to_strong(TWeakPtr<Extra...> const& weak)
{
    return weak.Pin();
}

// Unreal Engine TWeakObjectPtr
template<typename... Extra>
auto try_resolve_weak_pointer_to_strong(TWeakObjectPtr<Extra...> const& weak)
{
    return weak.Get();
}

// Qt QWeakPointer
template<typename... Extra>
auto try_resolve_weak_pointer_to_strong(QWeakPointer<Extra...> const& weak)
{
    return weak.toStrongRef();
}

Note that we are using the Extra... technique for accepting any specialization of a C++ template type.²

The case of wil is a little tricky because com_weak_ptr is itself a template alias. We have to use the name of the base template type rather than the type alias.

¹ If you decide to deepen the sad history of Visual Studios custom __if_exists keyword, you could write

template<typename Weak, typename Handler>
auto weak_delegate(
    Weak&& weak,
    Handler&& handler)
{
  return [weak = std::forward<Weak>(weak),
          handler = std::forward<Handler>(handler)]
    (auto&&... args) mutable
    {
      __if_exists(Weak::get) {
        auto strong = weak.get();
      }
      __if_exists(Weak::lock) {
        auto strong = weak.lock();
      }
      if (strong) {
        handler(std::forward<decltype(args)>(args)...);
      }
    };
}

But please, don’t do that. Use SFINAE instead.

² Others have noted that the Extra... trick doesn’t work for templates that take non-type template parameters or other templates, so maybe SFINAE is the way to go after all.

// winrt weak_ref
template<typename T>
auto try_resolve_weak_pointer_to_strong(T const& weak)
-> decltype(weak.get())
{
    return weak.get();
}

// std weak_ptr
template<typename T>
auto try_resolve_weak_pointer_to_strong(T const& weak)
-> decltype(weak.lock())
{
    return weak.lock();
}

// WRL WeakRef (no change)
auto try_resolve_weak_pointer_to_strong(::Microsoft::WRL::WeakRef const& weak)
{
    ::Microsoft::WRL::ComPtr<IInspectable> o;
    weak.As(&o);
    return o;
}

// wil com_weak_ptr
template<typename T>
auto try_resolve_weak_pointer_to_strong(T const& weak)
-> decltype(weak.try_copy<IUnknown>())
{
    return weak.try_copy<IUnknown>();
}

// Unreal Engine TWeakPtr
template<typename T>
auto try_resolve_weak_pointer_to_strong(T const& weak)
-> decltype(weak.Pin())
{
    return weak.Pin();
}

// Unreal Engine TWeakObjectPtr
// (Would also accidentally match WRL if not for the better overload above)
template<typename T>
auto try_resolve_weak_pointer_to_strong(T const& weak)
-> decltype(weak.Get())
{
    return weak.Get();
}

// Qt QWeakPointer
template<typename T>
auto try_resolve_weak_pointer_to_strong(T const& weak)
-> decltype(weak.toStrongRef())
{
    return weak.toStrongRef();
}

One of the risks of using SFINAE for duck typing is that you may detect things that you didn’t mean to. For example, here, we have a name collision: Both WRL WeakRef and Unreal Engine TWeakObjectPtr have a Get method, but they do different things. The WRL Get method returns the wrapped IWeakReference* (no resolving happens), whereas the Unreal Engine Get method resolves the weak pointer to a strong pointer. We have to make sure that the WRL specialization gets picked up for WRL WeakRef.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments

Discussion are closed.