Deconstructing function pointers in a C++ template, trying to address the calling convention conundrum

Raymond Chen

Raymond

Last time, we tried to extend our traits class for function pointers so that it accept functions in all different calling conventions. This worked out great, except that on some architectures, many of the calling conventions are actually identical, and this causes our various partial specializations to result in duplicate definitions, and the compiler gets mad, and everything falls apart.

What we want to do is remove the partial specializations for calling conventions which are identical to cdecl. (In practice, it is just cdecl that the other calling conventions collapse into.)

We start by detecting whether a particular specialization should be enabled.

struct CallingConventions
{
    using Cdecl = void(CC_CDECL*)();
    using Stdcall = void(CC_STDCALL*)();

    template<typename T>
    static constexpr bool IsCdecl = std::is_same_v<Cdecl, T>;

    static constexpr bool HasStdcall = !IsCdecl<Stdcall>;
};

We check whether various calling conventions are present by seeing if a function declared with that convention is the same as a function declared with cdecl. (This is why I made the various calling conventions be represented by a function pointer type.)

The usual way to remove things is to use std::enable_if, so let’s try that. Enable the specialization if cdecl and stdcall are different.

template<typename F> struct FunctionTraits;

template<typename R, typename... Args>
struct FunctionTraits<R(CC_CDECL*)(Args...)>
    : FunctionTraitsBase<R, Args...>
{
  using Pointer = R(CC_CDECL*)(Args...);
  using CallingConvention = CallingConventions::Cdecl;
};

template<typename R, typename... Args,
    typename = std::enable_if_t<
      !std::is_same_v<
        CallingConventions::Cdecl,
        CallingConventions::Stdcall>>
    >
struct FunctionTraits<R(CC_STDCALL*)(Args...)>
    : FunctionTraitsBase<R, Args...>
{
  using Pointer = R(CC_STDCALL*)(Args...);
  using CallingConvention = CallingConventions::Stdcall;
};

This fails to compile. First we get “default template arguments may not be used in partial specialization” on the typename = std::enable_if_t<...>. This error message is self-explanatory.

And then we get “redefinition of FunctionTraits<R(*)(Args...)>.” The problem is that in the case where cdecl and stdcall are the same, the partial specializations both try to define FunctionTraits<R(*)(Args...)>, so the compiler doesn’t know which one to choose.

Okay, so let’s put the enable_if in the second part.

template<typename F, typename = void>
struct FunctionTraits;

template<typename R, typename... Args>
struct FunctionTraits<R(CC_CDECL*)(Args...)>
    : FunctionTraitsBase<R, Args...>
{
  using Pointer = R(CC_CDECL*)(Args...);
  using CallingConvention = CallingConventions::Cdecl;
};

template<typename R, typename... Args>
struct FunctionTraits<R(CC_STDCALL*)(Args...),
    std::enable_if_t<
      !std::is_same_v<
        CallingConventions::Cdecl,
        CallingConventions::Stdcall>>
    >
    : FunctionTraitsBase<R, Args...>
{
  using Pointer = R(CC_STDCALL*)(Args...);
  using CallingConvention = CallingConventions::Stdcall;
};

This fails with “template argument 2 (std::enable_if_t<...>) is invalid.” This is a fair complaint, because it is indeed invalid. SFINAE does not apply here because there is no substitution going on. We flat-out passed an invalid type as a template parameter, and that’s an error.

But this gave me an idea. The problem with the first attempt is that we ended up with two partial specializations that ended up being the same thing. The problem with the second attempt is that we tried to create a bad partial specialization.

But maybe I can combine both bad ideas!

We’ll try that next time.

2 comments

Comments are closed.

  • Richard Smith
    Richard Smith

    It seems to me that your task would be easier if you use function overloading instead of class template specializations:

    template auto function_traits_impl(R(*)(Args...)) -> FunctionTraitsBase;
    template auto function_traits_impl(R(__cdecl*)(Args...)) -> FunctionTraitsBase;
    template auto function_traits_impl(R(__stdcall*)(Args...)) -> FunctionTraitsBase;
    template auto function_traits_impl(R(__fastcall*)(Args...)) -> FunctionTraitsBase;
    template using FunctionTraits = decltype(function_traits_impl((F*)nullptr));
    

    We don’t need to worry about collisions here, since they’ll just be harmless redeclarations of the same template. Overload sets can also be generated by pack expansion, which could save you some duplication (though in this case it’s probably not worth worrying about).

  • Avatar
    David Flicek

    OT: But I couldn’t find a suggestion box or contact. What I really want to know is why are the desktop icons are starting at x:13 and y:2 yet the left side is at 0 on the screen and the top is down about 4? If you take an icon on the right most column, add the spacing size, it goes beyond the end of the work area (monitor) (or virtual screen – single monitor), but if do the math taking the POINT x:2488 subtract 13 then divide by 75 (my desktop reported icon spacing via IFolderView2 is 75×100) it works out as the even multiple expected. So where does this mystery skew of 13 and 2 come from? I looked the the IShellView window rect starts at 0, 0 to end of screen, I checked the handle returned by IShellWindows::FindWindowSW and it was 0,0 to end of screen. Is it just hard coded or is there a way to get it? Thanks.