October 2nd, 2025
0 reactions

The problem with inferring from a function call operator is that there may be more than one

Some time ago, I wrote briefly on writing a helper class for generating a particular category of C callback wrappers around C++ methods. This particular mechanism used a proxy object with a templated conversion operator to figure out what function pointer type it was being asked to produce.¹

But what about taking a std::invoke‘able object and inferring the function pointer from the parameters that the invoke‘able’s operator() accepts?

Sure, you could try to do that, but there’s a catch: There might be more than one operator().

The common case of this is a lambda with auto parameters.

RegisterCallback(
    CallableWrapper([](auto first, auto second, auto third) {
        ⟦ ... ⟧
    }, context);

Formally, the lambda is a class with a templated function call operator.

class Lambda
{
    template<typename T1, typename T2, typename T3>
    auto operator()(T1 first, T2 second, T3 third) {
        ⟦ ... ⟧
    }
};

You can’t ask for the parameter types for the operator() because this class has infinitely many operator()s!

In practice, people often use auto not because they want an infinite number of operator() methods but because they just don’t want to be bothered writing out the data types.

In practice, when a library requires a callback function, it typically also defines a type name for the callback function signature, so specifying the callback function signature is normally not that onerous. (But we managed to avoid even that by deducing the callback function signature from the proxy.)

While I’m here, I may as well expand the Callback­Wrapper so that it takes normal function pointers, too.

template<typename F> struct MemberFunctionTraits;

template<typename Ret, typename T, typename...Args>
struct MemberFunctionTraits<Ret(T::*)(Args...)>
{
    using Object = T;
};

template<typename F> struct FunctionTraits;

template<typename T, typename Arg1, typename... Rest>
struct FunctionTraits<T(*)(Arg1, Rest...)>
{
    using First = Arg1;
}



template<auto F>
struct CallbackWrapperMaker
{
    template<typename Ret, typename...Args>
    static Ret callback(void* p, Args...args) {
        using FT = decltype(F);
        if constexpr (std::is_member_function_pointer_v<FT>) {
            auto obj = (typename MemberFunctionTraits<FT>::Object*)p;
            return (obj->*F)((Args)args...); }
        } else {
            auto obj = (typename FunctionTraits<FT>::First*)p;
            return F(obj, (Args)args...);                     
        }
    }

    template<typename Ret, typename...Args>
    using StaticCallback = Ret(*)(void*, Args...);

    template<typename Ret, typename...Args>
    operator StaticCallback<Ret, Args...>()
        { return callback<Ret, Args...>; }
};

template<auto F>
inline CallbackWrapperMaker<F> CallbackWrapper =
    CallbackWrapperMaker<F>();

¹ A danger of proxy objects with conversion operators is that people might not convert it right away. As I’ve noted before, the C++ language defines a specialization std::vector<bool> which represents a packed bit array, rather than defining a separate type like std::bitvector. This has made a lot of people very angry and has been widely regarded as a bad move.

One of the unfortunate features of std::vector<bool> is that the [] operator does not produce a reference to a bool. (The std::vector<bool> doesn’t even have any bools to return a reference to.) Instead, it returns a proxy object: If you convert the proxy object to a bool, then it reads a bit from the std::vector<bool>. If you assign a bool to the proxy object, then it sets/clears a bit in the std::vector<bool>.

But what if you don’t do either of those things?

extern std::vector<bool> GetBoolVector();

void oops()
{
    auto firstBit = GetBoolVector()[0];
    if (firstBit && SomeOtherCondition()) {
    }
        ⟦ ... ⟧
}

The firstBit is neither immediately assigned to nor converted. It’s just holds the potential of accessing the vector<bool>, and that potential is not realized until you do one of the two operations. In our case, we convert it to a bool, but it’s too late: The vector<bool> which it references no longer exists.

ERROR: AddressSanitizer: heap-use-after-free on address 0x77bab73e0010
    at pc 0x000000401722 bp 0x7ffcd6143bf0 sp 0x7ffcd6143be8
READ of size 8 at 0x77bab73e0010 thread T0
    #0 0x000000401721 in std::_Bit_reference::operator bool() const stl_bvector.h:106
    #1 0x00000040157f in oops() oops.cpp:17
    #2 0x0000004015f2 in main oops.cpp:22

Fortunately, our CallbackWrapperMaker doesn’t hold any references to other objects, so the proxy can be held indefinitely and converted when finally needed.

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