比较 C++20 中的多态类型

Comparing polymorphic types in c++20

我的代码介于 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 目前拥有的).

以上代码停止编译,因为实施了有关比较运算符的新规则。可逆运算符== 会导致歧义。我不能使用宇宙飞船运算符,因为它没有在版本 9 中实现。但是我在示例中省略了这一点——我假设任何与 == 一起工作的东西都将与其他运算符一起工作。

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

您的代码中没有任何多态性。您可以使用 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;

作为中间解决方案,您可以 re-factor 您的多态相等性 operator== 到 non-virtual operator== 中定义的基 class,多态分派到一个 non-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);

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

您可以在 Identifier 结构中添加一个抽象(纯虚拟)函数,returns 它的“身份”。

然后,在扩展 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-table的指针(因此我认为这种方法与多态性完全相反)。

修改后的答案如下:

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;
    }
};

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

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

一种更直观的表达方式可能是:类型是使标识符有意义的唯一因素。除非先将其向下转换为 MachineIdentifierUserIdentifier,否则您无法使用裸 Identifier 执行任何操作。因此 Identifier class 很可能是错误的,将 MachineIdentifierUserIdentifier 进行比较是编译器应该检测到的类型错误。

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

他们应该写什么?模板怎么样?模板实例不是模板的子类型,也不是彼此的子类型。如果你有这些标识符代表的 MachineUser 类型,你可以尝试编写一个模板 Identifier 结构并对其进行特殊化,而不是对其进行子class:

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 (also see )。当您第一次遇到它时有点莫名其妙,但它让您能够在 superclass 中引用特定的 subclass 类型(在本例中,如 Id ).它对您来说可能也是一个不错的选择,因为与大多数其他选项相比,它需要对已经正确使用 MachineIdentifierUserIdentifier.

的代码进行相对较少的更改

¹ 如果标识符 可以互换,那么这个答案的大部分(以及其他答案的大部分)可能不适用。不过如果是这样的话,应该也可以不用downcasting来进行比较吧。