比较 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;
}
我们现在正在迁移到 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
实际上 不是 标识符,因为它不标识任何东西!这是一个破碎的抽象。
一种更直观的表达方式可能是:类型是使标识符有意义的唯一因素。除非先将其向下转换为 MachineIdentifier
或 UserIdentifier
,否则您无法使用裸 Identifier
执行任何操作。因此 Identifier
class 很可能是错误的,将 MachineIdentifier
与 UserIdentifier
进行比较是编译器应该检测到的类型错误。
在我看来,Identifier
存在的最可能原因是因为有人意识到 MachineIdentifier
和 UserIdentifier
之间存在共同代码,并得出共同行为的结论应该提取到 Identifier
基本类型,特定类型继承自它。对于任何在学校学习过“继承使代码重用”但尚未意识到还有其他类型的代码重用的人来说,这是一个可以理解的错误。
他们应该写什么?模板怎么样?模板实例不是模板的子类型,也不是彼此的子类型。如果你有这些标识符代表的 Machine
和 User
类型,你可以尝试编写一个模板 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;
}
};
现在,比较 MachineIdentifier
和 UserIdentifier
是编译时错误。
此技术称为 curiously recurring template pattern (also see crtp)。当您第一次遇到它时有点莫名其妙,但它让您能够在 superclass 中引用特定的 subclass 类型(在本例中,如 Id
).它对您来说可能也是一个不错的选择,因为与大多数其他选项相比,它需要对已经正确使用 MachineIdentifier
和 UserIdentifier
.
的代码进行相对较少的更改
¹ 如果标识符 是 可以互换,那么这个答案的大部分(以及其他答案的大部分)可能不适用。不过如果是这样的话,应该也可以不用downcasting来进行比较吧。
我的代码介于 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;
}
我们现在正在迁移到 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
实际上 不是 标识符,因为它不标识任何东西!这是一个破碎的抽象。
一种更直观的表达方式可能是:类型是使标识符有意义的唯一因素。除非先将其向下转换为 MachineIdentifier
或 UserIdentifier
,否则您无法使用裸 Identifier
执行任何操作。因此 Identifier
class 很可能是错误的,将 MachineIdentifier
与 UserIdentifier
进行比较是编译器应该检测到的类型错误。
在我看来,Identifier
存在的最可能原因是因为有人意识到 MachineIdentifier
和 UserIdentifier
之间存在共同代码,并得出共同行为的结论应该提取到 Identifier
基本类型,特定类型继承自它。对于任何在学校学习过“继承使代码重用”但尚未意识到还有其他类型的代码重用的人来说,这是一个可以理解的错误。
他们应该写什么?模板怎么样?模板实例不是模板的子类型,也不是彼此的子类型。如果你有这些标识符代表的 Machine
和 User
类型,你可以尝试编写一个模板 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;
}
};
现在,比较 MachineIdentifier
和 UserIdentifier
是编译时错误。
此技术称为 curiously recurring template pattern (also see crtp)。当您第一次遇到它时有点莫名其妙,但它让您能够在 superclass 中引用特定的 subclass 类型(在本例中,如 Id
).它对您来说可能也是一个不错的选择,因为与大多数其他选项相比,它需要对已经正确使用 MachineIdentifier
和 UserIdentifier
.
¹ 如果标识符 是 可以互换,那么这个答案的大部分(以及其他答案的大部分)可能不适用。不过如果是这样的话,应该也可以不用downcasting来进行比较吧。