指向成员的指针映射

4

(注:如果这个问题让人感觉像是一个X-Y问题,请滚动到下面的分隔符了解我提出这个问题的原因)

我正在寻找一种方法来存储不同类型的成员函数指针并比较它们是否相等。我需要将指向成员函数的指针映射到任意对象,并在该映射中进行搜索。它不必是关联容器,线性搜索就可以。还要注意,这些指针仅用作映射键,从不被解引用。

我的当前方法是:在构建映射时,我将传入的成员指针重新解释为一个众所周知的类型(void (MyClass::*)()),然后将其插入到映射中。大致如下(为简洁起见省略了错误检查):

template <class R, class... A)
void CallChecker::insert(R (MyClass::*key)(A...), Object value)
{
  mapping.push_back(std::make_pair(reinterpret_cast<void (MyClass::*)()>(key), value));
}

然后在查找时,我执行相同的转换并通过等式搜索:

template <class R, class... A)
Object CallChecker::retrieve(R (MyClass::*key)(A...)) const
{
  auto k = reinterpret_cast<void (MyClass::*)()>(key);
  auto it = std::find_if(begin(mapping), end(mapping), [k](auto x) { return x.first == k; });
  return it->second;
}

然而,我不确定这种方法总是有效的。虽然我相信它不会产生错误的负面影响(将两个相等的指针报告为不同),但我担心它可能会产生错误的负面影响(当将两个原本不同类型的指针强制转换为“通用”类型时,它们可能会比较相等)。所以我的问题是,是否存在这种情况?或者我在使用这样的比较时是否安全?
我知道我正在接近未定义行为的领域。但是,我不介意使用标准未定义但在gcc和MSVC(我的两个目标编译器)中已知可行的行为来解决问题。
因此,问题是:通用类型的比较是否安全?还是说我最好将存储的指针强制转换为传入类型进行比较(如下所示):
template <class R, class... A)
Object CallChecker::retrieve(R (MyClass::*key)(A...)) const
{
  auto it = std::find_if(begin(mapping), end(mapping), [key](auto x) { return reinterpret_cast<R (MyClass::*)(A...)>(x.first) == key; });
  return it->second;
}

这两种方法实际使用起来都不可行,那我就没戏了吗?
我对指向成员的上述属性感兴趣,既是因为我的实际任务需要,也是为了加深我对该语言的理解。然而,出于完整性的考虑(以及万一有人知道更好的方法),以下是我提出这个问题的过程。
我正在构建一个实用框架来帮助单元测试Qt4信号(测试是否发出正确的信号)。我的想法是创建一个名为CallChecker的类,它将为插槽存储验证器(包装std :: function对象),并能够运行它们。测试将创建一个从此类派生的类;该类将定义运行相应验证器的插槽。以下是使用方法的示例(简化):
class MyTester : public QObject, public CallChecker
{
  Q_OBJECT
public slots:
  void slot1(int i, char c) { CallChecker::checkCall(&MyTester::slot1, i, c); }
  void slot2(bool b) { CallChecker::checkCall(&MyTester::slot2, b); }
};

void testRunner()
{
  MyTester t;
  connectToTestedSignals(t);
  t.addCheck(&MyTester::slot1, [](int i, char c) { return i == 7; });
}

我有一个工作实现(在ideone上使用gcc),其中CallChecker使用一对指针成员转换为通用函数类型的std::vector。通过一些编译器标志的尝试(/vmg),我也让它在MSVC上运行成功了。
如果您能提出比指针成员查找更好的解决方案,我将非常乐意听取。我的目标是使实现测试槽的类易于使用:我真的希望这些槽只是简单的一行代码。使用槽签名的文本表示法(Qt在内部使用)并不是一个很好的选择,因为它太容易出现拼写错误。

为什么有人需要这样做?这是一个X-Y问题,因为单元测试qt信号槽的方法不同。我可以回答如何正确地完成它,但我无法回答你的问题。 - BЈовић
1
还要注意,指针仅用作映射键,它们永远不会被取消引用。类型擦除呢?类似于 boost::any 这种类型吗? - user2485710
@BЈовић,请您这样做,我在问题中甚至都这样问了:“如果您能提出比成员指针查找更好的解决方案,我会很高兴听到它。” - Angew is no longer proud of SO
如果 'X' 是QT,对我来说这是没意义的。与指向成员函数指针合法操作相关的 'Y' 受到标准严格限制,在实践中也是如此。 - david.pfx
@david.pfx 好的,我可能表达得不够清楚。我主要是在问指向成员的指针部分(也出于对标准方面的好奇)。不过,我想把它放到我如何遇到这个问题的背景中去。我稍微编辑了一下问题,希望现在更加明确了重点。 - Angew is no longer proud of SO
4个回答

2
正如我在评论中所说,有一种方法可以对qt信号进行单元测试。您需要使用QSignalSpy并链接到QTestLib。
正如他们在文档中所说:
“QSignalSpy可以连接到任何对象的任何信号并记录其发射。 QSignalSpy本身是一个QVariant列表。信号的每个发射都将向列表附加一个项目,其中包含信号的参数。”
您也可以阅读他们的示例,但这里是我的一个单元测试,使用google test:
class TestSomeControls : public testing::Test
{
public:

    TestSomeControls() :
        obj(),
        ctrl1Dis( &obj, SIGNAL(DisableControl1(bool)) ),
        ctrl2Dis( &obj, SIGNAL(DisableControl2(bool)) )
    {
    }

    model::SomeControls obj;

    QSignalSpy ctrl1Dis;
    QSignalSpy ctrl2Dis;
};

TEST_F( TestSomeControls, OnControl1Clicked_untilControl1Disabled )
{
    for ( int i = 0; i < 5; ++ i )
    {
        obj.OnControl1Clicked();
        ASSERT_EQ( ctrl1Dis.count(), 0 );
    }

    obj.OnControl1Clicked();

    ASSERT_EQ( ctrl1Dis.count(), 1 );
    ASSERT_EQ( ctrl1Dis.takeFirst().at(0).toBool(), true );
}

好的,谢谢。这对于不是Qt元类型的信号参数类型有效吗? - Angew is no longer proud of SO
1
@Angew 不行,因为它必须存储在 QVariant 中。这是要求的。 - BЈовић
好吧,回到起点重新开始。不过,我会记住这个方法以备将来使用。谢谢。 - Angew is no longer proud of SO

2

比较任何东西与任何东西。

#include <utility>
#include <memory>
#include <iostream>

struct Base
{
  virtual bool operator== (const Base& other) const = 0;
  virtual ~Base() {}
};

template <class T>
struct Holder : Base
{
  Holder(T t) : t(t) {}
  bool operator== (const Base& other) const
  {
    const Holder<T>* h = dynamic_cast<const Holder<T>*>(&other);
    return (h && h->t == t);
  }
  private:
  T t;
};

struct Any
{
  template<class T>
    Any(T t) : p(std::make_shared<Holder<T>>(t)) {}
  bool operator== (const Any& other) const
  {
    return *p == *other.p;
  }
  private:
  std::shared_ptr<Base> p;
};

int main ()
{
  std::cout << (Any(2) == Any(2));
  std::cout << (Any(2) == Any(3));
  std::cout << (Any(2) == Any("foo"));
  std::cout << (Any("foo") == Any("foo"));
  std::cout << (Any("foo") == Any("bar"));
}

实现operator<的操作被推迟到读取器中。

重要提示:在此实现中,两个不同类型的成员指针将始终编译为不相等,但在转换为公共类型后直接比较时可能会相等。也就是说,如果Foo派生自Bar&Foo::x&Bar::x可能相同。此类行为不能轻易地添加到此处。


1
这是一个狭义问题的简短回答。
标准暗示并在脚注中说明了成员指针不能转换为void*。可能的理由是成员指针需要更多的存储字节,而void*不需要。您的编译器应禁止reinterpret_cast,即使它没有,您仍然会遇到真正的冲突风险。您可以在目标编译器上进行测试,但风险仍然存在。
标准将允许您将“类型为T1的X成员指针”转换为“类型为T2的Y成员指针”,当T1和T2都是函数类型时。换句话说,只要公共类型是成员函数指针,您的策略就是被允许的。我想这就是您的意图。 N3337中S5.2.10 / 10。然而,它并不保证这两个指针将比较相等,就像对象指针那样。例如,如果实现包含一个编码的“this”指针,它就行不通了。
标准将允许您在联合中存储成员指针。您可以提供一个足够长的char[]成员,并使用sizeof上的assert来确保它足够长。只要它是“标准布局”类型,通过char[]访问值应该具有保证的行为。个人而言,我会尝试一下,以了解这些指针实际上有多大!但是可能出现非规范值的问题仍然存在。
我的第三个建议是使用指向成员函数的typeid而不是指针本身。Typeid可以应用于任何表达式——如果对reinterpret_cast足够好,那么对于typeid也足够好——并且所得到的值应该是类型而不是实例的唯一值。
之后,我就没有其他想法了。您可能需要重新定义/重新协商问题,以寻找其他解决方案。

我正在将 reinterpret_cast 用于指向成员函数类型,并在问题中进行了说明。联合体的观点很好,但我记得你不能合法地读取不同于上一次写入的成员(标准布局结构有一个例外)。更不用说两个相等的指向成员的指针保证是位等的了。 - Angew is no longer proud of SO
你是对的。已编辑以反映这一点,并添加了typeid选项。 - david.pfx

1
如果您首先检查两侧的typeid是否相同,然后可以使用类型擦除函数将两侧转换为相同类型并在该类型中进行比较。 (这是按照标准严格要求的,因为即使您可以通过一个众所周知的类型往返,标准也不能保证该类型中的比较行为与原始类型中的比较行为相同。)以下是简要说明:
struct any_pmf_compare {
    std::type_index ti;
    void (any_pmf_compare::*pmf)();
    bool (*comp)(const any_pmf_compare &, const any_pmf_compare &);
    template<typename F>
    any_pmf_compare(F f):
        ti(typeid(F)),
        pmf(reinterpret_cast<void (any_pmf_compare::*)()>(f)),
        comp([](const any_pmf_compare &self, const any_pmf_compare &other) {
            return reinterpret_cast<F>(self.pmf) == reinterpret_cast<F>(other.pmf);
        })
    {
    }
};
bool operator==(const any_pmf_compare &lhs, const any_pmf_compare &rhs) {
    return lhs.ti == rhs.ti && lhs.comp(lhs, rhs);
}

谢谢。这非常接近我最终所做的事情,遵循@user2485710的评论。 - Angew is no longer proud of SO
抱歉,但我不认为这是保证可行的。标准并不保证两个成员函数指针相等,无论你如何去做。我的建议是使用typeid本身,而不是指针的值。 - david.pfx
@david.pfx 如果它们引用相同的成员,则确实如此(5.10p2)。以下示例非常清楚,这旨在适用于指向成员函数和数据成员的指针。 - ecatmur
@ecatmur:在我的阅读中没有这样的说法。完整的引用是//如果它们将引用同一最派生对象的同一成员//而函数不是一个对象(S1.8)。在示例(N3337)中,比较最终产生false,但这并不明显为什么会这样。这可能是标准中的缺陷,但至少它并没有明确保证两个指向函数的指针将永远相等。 - david.pfx
@david.pfx 标准规定是“相同的成员”,而不是“相同的对象”,因此并没有要求该成员必须是一个对象(它必须是一个对象的成员,但这并不是同一回事)。标准并不完全一致,但我认为根据慈善原则,在那段中我们必须把“成员”理解为指代一般成员,而不仅仅是数据成员。 - ecatmur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接