How well does WRL ComPtr support class template argument deduction (CTAD)?

Raymond Chen

Continuing our investigation of which C++ COM wrappers support class template argument deduction (CTAD), next up is WRL’s ComPtr.

WRL fails CTAD because it tries too hard. The from-raw-pointer constructor is this one:

template <typename T>
class ComPtr
{
public:
    typedef T InterfaceType;

protected:
    InterfaceType *ptr_;

public:
    template<class U>
    ComPtr(_In_opt_ U *other) throw() : ptr_(other) // this one
    {
        InternalAddRef();
    }

    ⟦ other stuff ⟧
};

WRL thinks is being helpful here in that it lets you, for example, initialize a ComPtr<IBase> not only with a pointer to IBase but also a pointer to anything that derives from IBase. If you pass a pointer to a derived class, it upcasts it into a pointer to the base class.

This attempt at being helpful prevents CTAD from working, because the compiler can’t figure out which T to use when it’s given just a U.

WRL could just have written the simple, straightforward constructor:

    ComPtr(_In_opt_ T *other) throw() : ptr_(other)
    {
        InternalAddRef();
    }

If the caller passes a pointer to something that doesn’t derive from T, then the compiler tells you that it can’t find a matching constructor.

This fancy template constructor is a case of what the Germans call Verschlimmbesserung, which means “making something worse in a well-intentioned but failed attempt to make it better.”

But I think I know why the authors of WRL wrote their raw-pointer constructor this way, and it’s a common problem to library authors: Error message metaprogramming.

If somebody tries to construct a ComPtr<T> from a pointer to something unrelated to T, the fancy templated version gives the error message

error C2440: 'initializing': cannot convert from 'U *' to 'IBase *'
with
    [
        U=INotDerived
    ]

Without the fancy template, the error message would have been

error C2440: '<function-style-cast>': cannot convert from 'INotDerived *' to 'Microsoft::WRL::ComPtr<IBase>'
'Microsoft::WRL::ComPtr<IBase>::ComPtr' no overloaded function could convert all the argument types
could be 'ComPtr<IBase>::ComPtr(const ComPtr<U> &amp,typename Details::EnableIf<Details::IsConvertible<U*, T*>::value, void *>::type *)'
with
    [
        T=IBase
    ]
or 'ComPtr<IBase>::ComPtr(nullptr)'
'ComPtr<IBase>::ComPtr(nullptr)': cannot convert argument 1 from 'INotDerived *' to 'nullptr'
or
    ⟦ repeat for the other constructors ⟧

A simple error on the client turns into an explosive and incomprehensible error message.

The library author can get the best of both worlds by providing both the fancy templated constructor (to improve the error message) and a simple, straightforward constructor (for CTAD):

template <typename T>
class ComPtr
{
public:
    typedef T InterfaceType;

protected:
    InterfaceType *ptr_;

public:
    // This one for CTAD
    ComPtr(_In_opt_ T *other) throw() : ptr_(other)
    {
        InternalAddRef();
    }

    // This one to improve error message
    template<class U>
    ComPtr(_In_opt_ U *other) throw() :
        // ComPtr(static_cast<T*>(other)) {}
        ComPtr(MustDeriveFromT(other)) {}

    ⟦ other stuff ⟧

private:
    constexpr static T* MustDeriveFromT(T* p) { return p; }
};

Or the library author can provide a deduction guide to steer CTAD to the correct type:

template<typename T> ComPtr(T*) -> ComPtr<T>;

Since WRL was written for C++11, you’d have to put the deduction guide inside an #ifdef to hide it from pre-C++17 compilers.

As a consumer, though, you shouldn’t be creating deduction guides for somebody else’s classes. Instead, you can use a maker function.

template<typename T>
ComPtr<T> MakeComPtr(T* p)
{
    return p;
}

Next time, we’ll look at wil, which has a different category of problems.

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • GL 0

    The suggested best-of-both-worlds is not good:

    template<class U>
    ComPtr(_In_opt_ U *other) throw() :
      ComPtr(static_cast<T*>(other)) { }

    This will allow construction of, say, ComPtr<IDispatch> from IUnknown * without compiler error. The fix is to remove the static_cast.

    • Neil Rashbrook 0

      If you remove the static cast then you’ve just made that constructor delegate to itself. While you can add a dummy disambiguation argument to the T* constructor, that just makes the error messages blow up, which defeats the object somewhat. There are a number of possible fixes:

      • a static assertion that T is a base class of U
      • creating a typedef for T* and replacing the static_cast with a function-style cast, which doesn’t allow downcasting (although unfortunately this does cause more verbose error messages)
      • replacing static_cast with the once proposed implicit_cast which also doesn’t allow downcasting
      #include <type_traits>
      #include <utility>
      template <class T, class U>
      constexpr T implicit_cast(U&& u) noexcept(std::is_nothrow_convertible_v<U, T>) {
        return std::forward<U>(u);
      }
      • GL 0

        You’re absolutely correct, another simple fix (in this case) is to duplicate the body:

        ComPtr(T *ptr) : ptr_{ptr} { InternalAddRef(); }
        
        template <typename U>
        ComPtr(U *other) : ptr_{other} { InternalAddRef(); }
      • Raymond ChenMicrosoft employee 0

        Excellent points everyone. I fixed the code so we use a helper function MustDeriveFromT.

Feedback usabilla icon