【问题标题】:Comparing polymorphic types in c++20比较 C++20 中的多态类型
【发布时间】:2021-02-09 04:06:21
【问题描述】:

我的代码介于 c++17 和 c++20 之间。具体来说,我们在 GCC-9 和 clang-9 上启用了 c++20,它只是部分实现。

在代码中,我们有相当大的多态类型层次结构,如下所示:

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

struct UserIdentifier : public Identifier {
    int userId =0;
    bool operator==(const Identifier&other) const override {
        const UserIdentifier *otherUser = dynamic_cast<const UserIdentifier*>(&other);
        return otherUser && otherUser->userId == userId;
    }
};

struct MachineIdentifier : public Identifier {
    int machineId =0;
    bool operator==(const Identifier&other) const override {
        const MachineIdentifier *otherMachine = dynamic_cast<const MachineIdentifier*>(&other);
        return otherMachine && otherMachine->machineId == machineId;
    }
};

int main() {
    UserIdentifier user;
    MachineIdentifier machine;
    return user==machine? 1: 0;
}

https://godbolt.org/z/er4fsK

我们现在正在迁移到 GCC-10 和 clang-10,但由于某些原因,我们仍然需要在版本 9 上工作(好吧,至少是 clang-9,因为这是 android NDK 目前所拥有的)。

上述代码停止编译,因为实施了有关比较运算符的新规则。可逆运算符== 会导致歧义。我不能使用 spaceship 运算符,因为它没有在版本 9 中实现。但我在示例中省略了这一点 - 我假设任何适用于 == 的东西都适用于其他运算符。

所以: 在 c++20 中使用多态类型实现比较运算符的推荐方法是什么?

【问题讨论】:

  • 您确定所有dynamic_cast 的用法吗?这与多态性 IMO 完全相反。此外,它还会产生额外的运行时影响。当然,这里有避免 RTTI 的方法。
  • 我担心这种方法是a == b 可能不等同于b == aExample
  • 我听到了,我也不太喜欢这种设计。 @FrançoisAndrieux 每种类型只能与相同类型的其他类型比较。我认为它应该是等价的,只要层次结构的叶子具有具体的运算符实现并且它们的行为都如示例所示。
  • @MateuszL 在这种情况下,将final 添加到叶类以及对该效果的注释可能有助于避免错误。
  • 这些真的是多态的吗?您是否有您不知道具体类型的Identifiers 向量?还是Identifier 只是通用代码的存储库?对于curiously recurring template pattern,这似乎是一个不错的应用程序

标签: c++ polymorphism comparison c++20 comparison-operators


【解决方案1】:

作为一种中间解决方案,您可以将多态相等 operator== 重构为基类中定义的非虚拟 operator==,它以多态方式分派给非运算符的虚拟成员函数:

struct Identifier {    
    bool operator==(const Identifier& other) const {
        return isEqual(other);
    }
private:
    virtual bool isEqual(const Identifier& other) const = 0;
};

// Note: do not derive this class further (less dyncasts may logically fail).
struct UserIdentifier final : public Identifier {
    int userId = 0;
private:
    virtual bool isEqual(const Identifier& other) const override {
        const UserIdentifier *otherUser = dynamic_cast<const UserIdentifier*>(&other);
        return otherUser && otherUser->userId == userId;
    }
};

// Note: do not derive this class further (less dyncasts may logically fail).
struct MachineIdentifier final : public Identifier {
    int machineId = 0;
private:
    virtual bool isEqual(const Identifier& other) const override {
        const MachineIdentifier *otherMachine = dynamic_cast<const MachineIdentifier*>(&other);
        return otherMachine && otherMachine->machineId == machineId;
    }
};

现在将不再有歧义,因为 isEqual 虚拟成员函数的调度将始终在 operator== 的左侧参数上完成。

const bool result = (user == machine);  // user.isEqual(machine);

【讨论】:

  • 请注意,此方法可以扩展为使用double dispatch,这样可以消除对dynamic_cast 的需要。
【解决方案2】:

好的,我看到@dfrib 给出的答案中没有提到它,所以我将扩展该答案以显示它。

您可以在Identifier 结构中添加一个抽象(纯虚)函数,该函数返回其“身份”。

然后,在扩展Identifier 结构的每个结构中,您可以调用该函数而不是动态转换输入对象并检查其类型是否与this 对象匹配。

当然,您必须确保完全区分每个结构的标识集。换句话说,任何两组身份都不能共享任何共同的值(即,这两组必须不相交)。

这将使您完全摆脱 RTTI,这与多态性 IMO 几乎完全相反,并且在此之上还会产生额外的运行时影响。

这是该答案的扩展:

struct Identifier {    
    bool operator==(const Identifier& other) const {
        return getVal() == other.getVal();
    }
private:
    virtual int getVal() const = 0;
};

struct UserIdentifier : public Identifier {
private:
    int userId = 0;
    virtual int getVal() const override {
        return userId;
    }
};

struct MachineIdentifier : public Identifier {
private:
    int machineId = 100;
    virtual int getVal() const override {
        return machineId;
    }
};

如果您想支持具有除int 之外的其他类型标识符的结构,那么您可以扩展此解决方案以使用模板。

除了为每个结构强制执行一组不同的身份之外,您还可以添加一个 type 字段,并确保只有该字段在不同结构中是唯一的。

本质上,这些类型相当于dynamic_cast检查,比较输入object的V-table的指针和输入结构的指针 的 V 表(因此我认为这种方法与多态性完全相反)。

这是修改后的答案:

struct Identifier {    
    bool operator==(const Identifier& other) const {
        return getType() == other.getType() && getVal() == other.getVal();
    }
private:
    virtual int getType() const = 0;
    virtual int getVal() const = 0;
};

struct UserIdentifier : public Identifier {
private:
    int userId = 0;
    virtual int getType() const override {
        return 1;
    virtual int getVal() const override {
        return userId;
    }
};

struct MachineIdentifier : public Identifier {
private:
    int machineId = 0;
    virtual int getType() const override {
        return 2;
    virtual int getVal() const override {
        return machineId;
    }
};

【讨论】:

  • 如果userIdmachineId 的值匹配,则UserIdentifier 将与MachineIdentifier 进行比较,这可能会带来无意的影响。
  • @dfrib:这就是为什么我明确指出:“当然,您必须确保完全区分每个结构的标识集。换句话说,任何两个标识集不得共享任何共同值(即,两个集合必须不相交)。”。
  • 在您的示例中,即使默认身份也设置为相同的值,因此没有重叠机制(这可能会使读者感到困惑)。我认为您的答案涵盖了一种通常很好的替代方法,但是您可能希望拆分 id 和标识符标签的逻辑,后者将唯一地与类型的 kind 相关,无论前者是只是一个可能在不同类型的 id 之间重叠的 id。
  • @dfrib:是的,这是复制粘贴错误。顺便说一句,您还可以添加一个额外的类型字段,并允许自己在不同的结构中使用相同的身份,只要类型是唯一的。从本质上讲,这些类型相当于dynamic_cast 检查,它比较输入对象的 V-table 的指针和输入类型的 V-table 的指针。
  • 我同意,稍后我可能会更新/扩展答案,以显示使用标识符唯一标签的 dyncast 替代方案(目前在会议中,以便稍后使用)。
【解决方案3】:

这看起来不像是多态性问题。实际上,我认为任何多态性都是数据模型错误的症状。

如果您有标识机器的值和标识用户的值,并且这些标识符不可互换¹,则它们不应共享一个超类型。 “作为标识符”的属性是关于如何在数据模型中使用类型来识别另一种类型的值的事实。 MachineIdentifier 是一个标识符,因为它标识了一台机器; UserIdentifier 是一个标识符,因为它标识了一个用户。但Identifier 实际上不是标识符,因为它不识别任何东西!这是一个破碎的抽象。

更直观的说法可能是:类型是唯一使标识符有意义的东西。除非您先将其向下转换为 MachineIdentifierUserIdentifier,否则您无法对裸露的 Identifier 做任何事情。所以拥有Identifier 类很可能是错误的,将MachineIdentifierUserIdentifier 进行比较是编译器应该检测到的类型错误。

在我看来,Identifier 存在的最可能原因是有人意识到 MachineIdentifierUserIdentifier 之间存在共同代码,并得出结论应该将共同行为提取到 Identifier基类型,特定类型继承自它。对于那些在学校学到“继承使代码重用”并且还没有意识到还有其他类型的代码重用的人来说,这是一个可以理解的错误。

他们应该改写什么?模板怎么样?模板实例不是模板的子类型或彼此的子类型。如果您有这些标识符表示的类型 MachineUser,您可以尝试编写模板 Identifier 结构并对其进行专门化,而不是对其进行子类化:

template <typename T>
struct Identifier {};

template <>
struct Identifier<User> {
  int userId = 0;
  bool operator==(const Identifier<User> &other) const {
    return other.userId == userId;
  }
};

template <>
struct Identifier<Machine> {
  int machineId = 0;
  bool operator==(const Identifier<Machine> &other) const {
    return other.machineId == machineId;
  }
};

当您可以将所有数据和行为移动到模板中并且因此不需要专门化时,这可能是最有意义的。否则,这不一定是最佳选择,因为您不能指定 Identifier 实例化必须实现 operator==。我认为可能有一种方法可以使用 C++20 概念来实现这一点,或者类似的东西,但是,让我们将模板与继承结合起来,以获得两者的一些优势:

template <typename Id>
struct Identifier {
  virtual bool operator==(const Id &other) const = 0;
};

struct UserIdentifier : public Identifier<UserIdentifier> {
  int userId = 0;
  bool operator==(const UserIdentifier &other) const override {
    return other.userId == userId;
  }
};

struct MachineIdentifier : public Identifier<MachineIdentifier> {
  int machineId = 0;
  bool operator==(const MachineIdentifier &other) const override {
    return other.machineId == machineId;
  }
};

现在,比较 MachineIdentifierUserIdentifier 是编译时错误。

这种技术称为curiously recurring template pattern(另见)。当您第一次遇到它时,您会感到有些莫名其妙,但它为您提供了在超类中引用特定子类类型的能力(在此示例中,为Id)。这对您来说可能也是一个不错的选择,因为与大多数其他选项相比,它只需要对已经正确使用 MachineIdentifierUserIdentifier 的代码进行相对较少的更改。


¹ 如果标识符 可互换的,那么这个答案的大部分(以及大多数其他答案)可能不适用。但如果是这样的话,也应该可以在不向下转换的情况下比较它们。

【讨论】:

【解决方案4】:

您的代码中没有任何多态性。您可以通过使用Identifier 指针或引用来强制比较运算符函数(多态性)的动态绑定。

例如,而不是

UserIdentifier user;
MachineIdentifier machine;
return user==machine? 1: 0;

参考你可以做的:

UserIdentifier user;
MachineIdentifier machine;
Identifier &iUser = user;

return iUser == machine ? 1: 0;

反之,你可以显式调用UserIdentifier的比较运算符:

return user.operator==(machine) ? 1: 0;

【讨论】:

    猜你喜欢
    • 2019-09-28
    • 2013-11-21
    • 2023-03-26
    • 2012-09-14
    • 1970-01-01
    • 1970-01-01
    • 2015-02-10
    • 2020-01-06
    • 2014-08-19
    相关资源
    最近更新 更多