November 13th, 2025
0 reactions

Could we use CTAD to simplify the use of WRL’s Callback function?

Commenter Bwmat asked, “Can CTAD avoid the need for the lengthy type name in the last example?” This was in response to the rather lengthy type in the explicit specialization

void MyClass::RegisterCompletion(ABI::IAsyncAction* action)
{
    m_showingToken = inputPane->put_Showing(
        Microsoft::WRL::Callback<ABI::ITypedEventHandler<
            ABI::InputPane*,                             
            ABI::InputPaneVisibilityEventArgs*>>(        
            this, &MyClass::OnInputPaneShowing).Get());
}

Now, you cannot literally use CTAD (Class Template Argument Deduction) here because Callback is not a class.

Class template argument deduction exists to cover for the lack of type deduction in class naming. But functions do have type deduction! So let’s use it.

We’re starting with this:

template<
   typename TDelegateInterface,
   typename TCallbackObject,
   typename... TArgs
>
ComPtr<TDelegateInterface> Callback(
   _In_ TCallbackObject *object,
   _In_ HRESULT (TCallbackObject::* method)(TArgs...)
);

We want to deduce the TDelegateInterface to be TypedEventHandler<TArgs...>:

template<
   typename TDelegateInterface
        = TypedEventHandler<TArgs...>,
   typename TCallbackObject,
   typename... TArgs
>
ComPtr<TDelegateInterface> Callback(
   _In_ TCallbackObject *object,
   _In_ HRESULT (TCallbackObject::* method)(TArgs...)
);

Unfortunately, this doesn’t work because template default parameters cannot refer to future template parameters.

We could try reordering the parameters.

template<
   typename TCallbackObject,         
   typename... TArgs,                
   typename TDelegateInterface       
        = TypedEventHandler<TArgs...>
>
ComPtr<TDelegateInterface> Callback(
   _In_ TCallbackObject *object,
   _In_ HRESULT (TCallbackObject::* method)(TArgs...)
);

However, this doesn’t work because template parameter packs must come at the end of the template parameter list.

We can finesse the problem by splitting the template into one that specifically has exactly two TArgs and one for the other cases.

template<
   typename TDelegateInterface,
   typename TCallbackObject,
   typename... TArgs,
>
ComPtr<TDelegateInterface> Callback(
   _In_ TCallbackObject *object,
   _In_ HRESULT (TCallbackObject::* method)(TArgs...)
);

template<
    typename TCallbackObject,
    typename TArg1, typename TArg2,
    typename TDelegateInterface =
        TypedEventHandler<TArg1, TArg2>
>
ComPtr<TDelegateInterface> Callback(
   _In_ TCallbackObject *object,
   _In_ HRESULT (TCallbackObject::* method)(TArgs...)
);

Unfortunately, this fails due to the ambiguous call. We have to remove the first one from consideration when the number of parameters to the callback is exactly two.

template<
   typename TDelegateInterface,
   typename TCallbackObject,
   typename... TArgs,
>
std::enable_if_t<sizeof...(TArgs) != 2,
    ComPtr<TDelegateInterface>>
Callback(
   _In_ TCallbackObject *object,
   _In_ HRESULT (TCallbackObject::* method)(TArgs...)
);

template<
    typename TCallbackObject,
    typename TArg1, typename TArg2,
    typename TDelegateInterface =
        TypedEventHandler<TArg1, TArg2>
>
ComPtr<TDelegateInterface> Callback(
   _In_ TCallbackObject *object,
   _In_ HRESULT (TCallbackObject::* method)(TArgs...)
);

But even with this version, the two-argument callback has poor ergonomics: If you want to specify a custom delegate interface (like, say, Suspending­Event­Handler), you have to slog through the first three parameters so you can finally specify the last one.

Callback<MyObject, IInspectable*, SuspendingEventArgs*, SuspendingEventHandler>(
    this, &MyObject::OnSuspending);

Instead, we can use type deduction from the future.

template<
   typename TDelegateInterface = void,
   typename TCallbackObject,
   typename... TArgs
>
ComPtr<std::conditional_t<
    std::is_same_v<TDelegateInterface, void>,
    TypedEventHandler<TArgs...>,
    TDelegateInterface>>
Callback(
   _In_ TCallbackObject *object,
   _In_ HRESULT (TCallbackObject::* method)(TArgs...)
);

But wait, the Typed­Event­Handler takes only two template arguments, so if we invoke Callback with a pointer to member function that takes only one argument, we get a compiler error because it cannot form TypedEventHandler<TArg>.

struct MyObject
{
    HRESULT OnUIInvoked(IUICommand* command);
};

Callback<UICommandInvokedHandler<(this, &MyObject::OnUIInvoked);
// ^^^ error: template argument deduction/substitution failed
//            wrong number of template arguments to TypedEventHandler

We have to delay the mention of TypedEventHandler<TArgs...> until we are committed to using it.

template<typename... TArgs>                  
struct TypedEventHandlerHolder               
{                                            
    using type = TypedEventHandler<TArgs...>;
};                                           

template<
   typename TDelegateInterface = void,
   typename TCallbackObject,
   typename... TArgs
>
ComPtr<typename std::conditional_t<
    std::is_same_v<TDelegateInterface, void>,
    TypedEventHandlerHolder<TArgs...>,     
    std::type_identity<TDelegateInterface>>
        ::type                             
>
Callback(
   _In_ TCallbackObject *object,
   _In_ HRESULT (TCallbackObject::* method)(TArgs...)
);

Now, back at the top, I mentioned that CTAD doesn’t apply because CTAD is for class templates, but Callback is a function template.

But what if we made Callback a class template?

template<
    typename TDelegateInterface,
    typename TCallbackObject,
    typename... TArgs
>
struct Callback : ComPtr<TDelegateInterface>
{
    Callback(TCallbackObject *object,
             HRESULT (TCallbackObject::* method)(TArgs...));
};

Now, CTAD will infer the TCallbackObject and TArgs, and we can use a deduction guide to infer the TDelegateInterface.

template<
    typename TCallbackObject,
    typename TArg1, TArg2
>
Callback(TCallbackObject*,
         HRESULT (TCallbackObject::*)(TArg1, TArg2))
    -> Callback<TypedEventHandler<TArg1, TArg2>, TCallbackObject, TArg1, TArg2>;

Unfortunately, CTAD doesn’t work with partial specialization, so you can’t write

Callback<SuspendingEventHandler>(this, &MyObject::OnSuspending);

So I guess we’re stuck with the function overload with type deduction from the future.

But really, I think the winning move is not to play.

Instead of trying to make Callback fancy, create a separate function TypedEventHandlerCallback.

template<
   typename TCallbackObject,
   typename TArg1, typename TArg2>
ComPtr<TypedEventHandler<TArg1, TArg2>>
TypedEventHandlerCallback(
    TCallbackObject* object,
    HRESULT (TCallbackObject::*method)(TArg1, TArg2));

Then the original code would be

void MyClass::RegisterCompletion(ABI::IAsyncAction* action)
{
    m_showingToken = inputPane->put_Showing(
        TypedEventCallback(this, &MyClass::OnInputPaneShowing).Get());
}

While we’re there, we can also make an EventHandlerCallback function.

template<
   typename TCallbackObject,
   typename TArg>
ComPtr<EventHandler<TArg>>
EventHandlerCallback(
    TCallbackObject* object,
    HRESULT (TCallbackObject::*method)(IInspectable*, TArg));
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