Matthew
Matthew

Reputation: 2792

What's the "right" way to type-erase a member function pointer?

I am writing a test fixture which involves ensuring certain callbacks are called at appropriate times (actually Qt signals, but it shouldn't matter for the sake of my problem). To help with this, I created a helper class that records when a callback (signal) fires into a list.

This list needs to be able to record which callback (signal) fired. I would also prefer to not need to create a new enumeration specifically for this purpose. My idea was to instead record the address of the signal as a type-erased pointer so I can check the record against the address of the signal.

To make things a little easier on myself, I record the signal type as a:

template <typename Object>
class SignalType
{
public:
  SignalType() = default;
  SignalType(SignalType const&) = default;
  SignalType(SignalType&&) = default;

  template <typename R, typename... Args>
  SignalType(R (Object::*member)(Args...))
    : member{reinterpret_cast<void (Object::*)()>(member)} {}

  template <typename R, typename... Args>
  bool operator==(R (Object::*other)(Args...)) const
  { return this->member == reinterpret_cast<void (Object::*)()>(other); }

private:
  void (Object::*member)() = nullptr;
};

This "hides" the type erasure from the point of use, so I can later just write:

QCOMPARE(event.type, &SomeObject::someMethod);

...without needing to clutter that with a cast.

However, GCC is unhappy:

warning: cast between incompatible pointer to member types from ‘void (SomeObject::*)(...)’ to ‘void (SomeObject::*)()’ [-Wcast-function-type]

Is there a way to make GCC happy without resorting to diagnostic #pragmas to simply shut up the warning? Is there some other, "better" way to achieve this particular flavor of type-erasure? (Note that I don't need to ever call the member that SignalType encapsulates; I just need to be able to test for equality.)


Sigh. Should search on the warning message, not what I'm trying to do. Technically I guess this is a duplicate of Cast Between Incompatible Function Types in gcc, however that only asks how to get rid of the warning, and isn't clear what the code is trying to accomplish. So, in order that I might learn something useful here, please focus on if there is some other, "cleaner" way to accomplish my goal rather than just closing this as a duplicate and saying "it can't be fixed".

Upvotes: 4

Views: 912

Answers (1)

Artyer
Artyer

Reputation: 40836

Here is a solution that holds a function pointer that can compare two values of the same type whilst also acting as a std::type_info checking at run time if two types are the same. It stores the function pointer in a char[].

#include <new>

template<typename Object>
class SignalType
{
public:
  SignalType() = default;
  SignalType(SignalType const&) = default;
  SignalType& operator=(SignalType const&) = default;

  template<typename R, typename... Args>
  SignalType(R (Object::*member)(Args...)) noexcept
    : comparator(&compare_members_from_void_ptr<R, Args...>) {
    using member_ptr_type = R(Object::*)(Args...);
    static_assert(sizeof(member_ptr_type) <= sizeof(void(Object::*)()), "Member pointer type too large?");
    static_assert(alignof(member_ptr_type) <= alignof(void(Object::*)()), "Member pointer align too large?");
    // Don't need to destruct since it has a trivial destructor
    new (member_storage) member_ptr_type(member);
  }

  bool operator==(const SignalType& other) const {
    if (!comparator) return !other.comparator;  // Check both empty
    // Same comparator implies same type
    return comparator == other.comparator && comparator(member_storage, other.member_storage);
  }
  bool operator!=(const SignalType& other) const {
    return !(*this == other);
  }

   // Return true if these contain pointers to members of the same type
  bool is_same_type_as(const SignalType& other) const {
    return comparator == other.comparator;
  }

  // true if holding a typed pointer (could still be nullptr)
  explicit operator bool() const {
    return comparator;
  }

  // Check if holding an `R(Object::*)(Args...)`
  template<typename R, typename... Args>
  bool is_type() const noexcept {
    return comparator && comparator == &compare_members_from_void_ptr<R, Args...>;
  }

  // Returns the held function pointer if it is of type R(Object::*)(Args...), else nullptr
  template<typename R, typename... Args>
  R(Object::* get() const noexcept)(Args...) {
    return is_type<R, Args...>() ? *static_cast<R(Object::**)(Args...)>(static_cast<void*>(member_storage)) : nullptr;
  }
private:
  alignas(void(Object::*)()) char member_storage[sizeof(void(Object::*)())];
  bool (*comparator)(const void*, const void*) = nullptr;

  template<typename R, typename... Args>
  static bool compare_members_from_void_ptr(const void* a, const void* b) noexcept {
    return *static_cast<R(Object::*const *)(Args...)>(a) == *static_cast<R(Object::*const *)(Args...)>(b);
  }
};

Upvotes: 2

Related Questions