类型擦除成员函数指针的“正确”方法是啥?

Posted

技术标签:

【中文标题】类型擦除成员函数指针的“正确”方法是啥?【英文标题】:What's the "right" way to type-erase a member function pointer?类型擦除成员函数指针的“正确”方法是什么? 【发布时间】:2020-05-12 17:38:40 【问题描述】:

我正在编写一个测试夹具,其中涉及确保在适当的时间调用某些回调(实际上是 Qt 信号,但为了我的问题它应该无关紧要)。为了解决这个问题,我创建了一个帮助类,用于记录回调(信号)何时触发到列表中。

这个列表需要能够记录哪个回调(信号)被触发。我也希望不需要专门为此目的创建一个新的枚举。我的想法是将信号的地址记录为类型擦除的指针,以便我可以根据信号的地址检查记录。

为了方便我自己,我将信号类型记录为:

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...))
    : memberreinterpret_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;
;

这从使用点“隐藏”了类型擦除,所以我以后可以写:

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

...不需要用演员表弄乱它。

但是,GCC 不高兴:

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

有没有一种方法可以让 GCC 满意,而无需求助于诊断 #pragmas 来简单地关闭警告?有没有其他“更好”的方法来实现这种特殊的类型擦除? (请注意,我不需要调用 SignalType 封装的成员;我只需要能够测试是否相等。)


叹息。应该搜索警告消息,而不是我想要做的。 技术上我猜这是Cast Between Incompatible Function Types in gcc 的副本,但是它只询问如何摆脱警告,并且不清楚代码试图完成什么。因此,为了让我在这里学到一些有用的东西,请关注是否有其他“更清洁”的方式来实现我的目标,而不是仅仅将其作为重复项关闭并说“它无法修复”。

【问题讨论】:

您可以使用std::any。如果对指向成员类型的相同指针的任意转换失败,那么您就知道它不是同一个函数。如果成功,则比较这些值。 @eerorika,不幸的是,这需要在 GCC 4.8 上编译; std::any 在 C++17 之前不可用...并且使用 boost 绝对不可能。也就是说,std::any 如何避免内部出现同样的问题? (或者是吗?) gcc 4.8 比较老了,可以考虑升级一下吗? 为什么,如果你不介意memcmp,那么你不应该介意memcpy 进入char data[sizeof(void (Object::*)())] @Matthew 你问的是“正确的方法”,但如果你不喜欢这个问题的答案,因为它使用了虚函数和堆分配,你应该在你的问题中编辑那个细节。 【参考方案1】:

这是一个解决方案,它包含一个函数指针,它可以比较相同类型的两个值,同时还充当std::type_info 在运行时检查两种类型是否相同。它将函数指针存储在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);
  
;

【讨论】:

附加类型安全的价值是什么? AFAIK,两个不同的成员函数永远不能有相同的地址? @Matthew 我做了一些思考,我认为 memcmp 将始终与== 相同,只要它们不是 nullptrs。两个指向成员的空指针函数可以比较不等于memcmp(参见godbolt.org/z/KtTBLP),但可能会有更多的边缘情况,所以这是符合标准的安全方法

以上是关于类型擦除成员函数指针的“正确”方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

这样做的正确方法是啥?调用指针到指针到指针中的函数?

通过带有成员函数指针的 QHash 调用成员函数的正确方法

C++|详解类成员指针:数据成员指针和成员函数指针及应用场合

访问包含在数组中的指向类对象的类成员函数的正确语法是啥?

C++ 类成员函数指针的使用方法

如何正确地将指针传递给第 3 方代码的成员函数? [复制]