防止兄弟结构的平等比较
Prevent equality comparison of sibling structs
出于代码重用的目的,我有许多从同一基派生的结构,但我不想要任何形式的多态性。
struct B {
int field;
void doStuff() {}
bool operator==(const B& b) {
return field == b.field;
}
};
struct D1 : public B {
D1(int field) : B{field} {}
};
struct D2 : public B {
D2(int field) : B{field} {}
};
结构 D1
和 D2
(以及更多类似的结构)派生自 B
以共享公共字段和方法,这样我就不需要在中复制这些字段和方法每个派生 类.
Struct B
从未被实例化;我只使用 D1
和 D2
的实例。此外,D1
和 D2
根本不应该相互交互。本质上,我不想要任何多态行为:D1
和 D2
,出于所有目的,应该充当不相关的结构。
我希望将任何 D1
与其他 D1
比较是否相等,将任何 D2
与其他 D2
进行比较是否相等。由于 D1
和 D2
不包含任何字段,因此在结构 B
.
中定义相等运算符似乎是合适的
但是,(如预期的那样)我在 D1
和 D2
之间得到以下交互:
int main() {
D1 d1a(1);
D1 d1b(1);
D2 d2(1);
assert(d1a == d1b); // good
assert(d1a == d2); // oh no, it compiles!
}
我不想将 D1
与 D2
对象进行比较,因为出于所有目的,它们应该表现得好像它们是无关的。
如何在不复制代码的情况下使最后一个断言成为编译错误?为 D1
和 D2
(以及所有其他类似结构)分别定义相等运算符将意味着代码重复,因此我希望尽可能避免这种情况。
您可以使用 CRTP 仅在对最终类型的基 class 的引用上定义 operator ==
:
template<typename T>
struct B {
int field;
void doStuff() {}
bool operator==(const B<T>& b) {
return field == b.field;
}
};
struct D1 : public B<D1> {
D1(int field) : B{field} {}
};
struct D2 : public B<D2> {
D2(int field) : B{field} {}
};
这会导致第一个 assert
编译而第二个被拒绝。
您可以从结构的原始定义中删除相等运算符,并将其替换为接受两个相同参数类型的函数模板:
template <class T> bool operator == (const T& lhs, const T& rhs)
{
return lhs.field == rhs.field;
}
请注意,此功能有点 "greedy",最好将其放在命名空间中(与结构一起启用 ADL)或进一步限制类型,如下所示:
#include <type_traits>
template <class T, std::enable_if_t<std::is_base_of_v<B, T>, int> = 0>
bool operator == (const T& lhs, const T& rhs)
{
return lhs.field == rhs.field;
}
(注意 std::is_base_of_v
需要 C++17,但从 C++11 开始就存在冗长的对应物)。
作为最后的调整,为了防止这样的显式实例化:
operator == <B>(d1a, d2); // ultra-weird usage scenario, but compiles!
或(正如@Aconcagua 在评论中指出的那样)使用 base-class 对派生结构的引用进行类型推导,
B& b1 = d1a;
B& b2 = d2;
assert(b1 == b2); // Compiles, but see below.
您可能还想添加
template <> bool operator == <B>(const B&, const B&) = delete;
"Structs D1 and D2 (and more similar structs) derive from B to share common fields and methods"
然后使B
成为private
基础class。显然,D1
和 D2
不应该共享它们的相等运算符,因为这两个运算符采用不同的参数。您当然可以将部分实现共享为 bool B::equal(B const&) const
,因为外部用户无法访问它。
不是将相等运算符定义为基础 class 的一部分,您通常需要派生 classes 中的两个函数:
struct B {
int field;
void doStuff() {}
};
struct D1 : public B {
D1(int field) : B{field} {}
bool operator==(const D1& d) {
return field == d.field;
}
};
struct D2 : public B {
D2(int field) : B{field} {}
bool operator==(const D2& d) {
return field == d.field;
}
};
或者,按照通常的偏好,您可以将它们设为免费函数:
bool operator==(const D1 &lhs, const D1 &rhs)
{
return lhs.field == rhs.field;
}
bool operator==(const D2 &lhs, const D2 &rhs)
{
return lhs.field == rhs.field;
}
注意:如果field
不是public成员,您需要将自由功能版本声明为friend
。
处理大量任意类型
好吧,也许您还有 D3
到 D99
,其中一些是 B
的间接后代。您需要使用模板:
template <class T>
bool operator==(const T &lhs, const T &rhs)
{
return lhs.field == rhs.field;
}
太棒了!但这抓住了 一切 ,这对于 不 应该具有可比性的不相关类型是不利的。所以我们需要约束。
TL;DR;解决方案
这是一个简单的实现,没有任何代码重复(即适用于任意数量的派生类型):
template <class T, class = std::enable_if<std::is_base_of<B,T>()
&& !std::is_same<B, std::remove_cv_t<std::remove_reference_t<T>>>()>>
bool operator==(const T &lhs, const T &rhs)
{
return lhs.field == rhs.field;
}
enable_if
首先检查 T
继承自 B
然后确保它 不是 B
。你在问题中说 B
基本上是一个抽象类型,从不直接实现,但它是一个编译时测试,所以为什么不偏执呢?
正如您后来在评论中指出的那样,并非所有 D#
都直接派生自 B
。 这仍然有效。
为什么你会遇到这个问题
鉴于以下情况:
D1 d1(1);
D2 d2(2);
d1 == d2;
编译器必须找到比较运算符,无论是自由函数还是 D1
的成员(不是 D2
)。值得庆幸的是,您已经在 class B
中定义了一个。上面的第三行可以等价地表述为:
d1.operator==(d2)
然而,operator==
是 B
的一部分,因此我们基本上调用 B::operator==(const B &)
。为什么当 D2
不是 B
时这会起作用?语言律师会澄清它在技术上是参数依赖查找 (ADL) 还是重载解析,但效果是 D2
作为函数调用的一部分静默转换为 B
,这相当于上面的:
d1.operator==(static_cast<B>(d2));
出现这种情况是因为找不到更好的比较函数。由于别无选择,编译器选择 B::operator==(const B &)
并进行转换。
出于代码重用的目的,我有许多从同一基派生的结构,但我不想要任何形式的多态性。
struct B {
int field;
void doStuff() {}
bool operator==(const B& b) {
return field == b.field;
}
};
struct D1 : public B {
D1(int field) : B{field} {}
};
struct D2 : public B {
D2(int field) : B{field} {}
};
结构 D1
和 D2
(以及更多类似的结构)派生自 B
以共享公共字段和方法,这样我就不需要在中复制这些字段和方法每个派生 类.
Struct B
从未被实例化;我只使用 D1
和 D2
的实例。此外,D1
和 D2
根本不应该相互交互。本质上,我不想要任何多态行为:D1
和 D2
,出于所有目的,应该充当不相关的结构。
我希望将任何 D1
与其他 D1
比较是否相等,将任何 D2
与其他 D2
进行比较是否相等。由于 D1
和 D2
不包含任何字段,因此在结构 B
.
但是,(如预期的那样)我在 D1
和 D2
之间得到以下交互:
int main() {
D1 d1a(1);
D1 d1b(1);
D2 d2(1);
assert(d1a == d1b); // good
assert(d1a == d2); // oh no, it compiles!
}
我不想将 D1
与 D2
对象进行比较,因为出于所有目的,它们应该表现得好像它们是无关的。
如何在不复制代码的情况下使最后一个断言成为编译错误?为 D1
和 D2
(以及所有其他类似结构)分别定义相等运算符将意味着代码重复,因此我希望尽可能避免这种情况。
您可以使用 CRTP 仅在对最终类型的基 class 的引用上定义 operator ==
:
template<typename T>
struct B {
int field;
void doStuff() {}
bool operator==(const B<T>& b) {
return field == b.field;
}
};
struct D1 : public B<D1> {
D1(int field) : B{field} {}
};
struct D2 : public B<D2> {
D2(int field) : B{field} {}
};
这会导致第一个 assert
编译而第二个被拒绝。
您可以从结构的原始定义中删除相等运算符,并将其替换为接受两个相同参数类型的函数模板:
template <class T> bool operator == (const T& lhs, const T& rhs)
{
return lhs.field == rhs.field;
}
请注意,此功能有点 "greedy",最好将其放在命名空间中(与结构一起启用 ADL)或进一步限制类型,如下所示:
#include <type_traits>
template <class T, std::enable_if_t<std::is_base_of_v<B, T>, int> = 0>
bool operator == (const T& lhs, const T& rhs)
{
return lhs.field == rhs.field;
}
(注意 std::is_base_of_v
需要 C++17,但从 C++11 开始就存在冗长的对应物)。
作为最后的调整,为了防止这样的显式实例化:
operator == <B>(d1a, d2); // ultra-weird usage scenario, but compiles!
或(正如@Aconcagua 在评论中指出的那样)使用 base-class 对派生结构的引用进行类型推导,
B& b1 = d1a;
B& b2 = d2;
assert(b1 == b2); // Compiles, but see below.
您可能还想添加
template <> bool operator == <B>(const B&, const B&) = delete;
"Structs D1 and D2 (and more similar structs) derive from B to share common fields and methods"
然后使B
成为private
基础class。显然,D1
和 D2
不应该共享它们的相等运算符,因为这两个运算符采用不同的参数。您当然可以将部分实现共享为 bool B::equal(B const&) const
,因为外部用户无法访问它。
不是将相等运算符定义为基础 class 的一部分,您通常需要派生 classes 中的两个函数:
struct B {
int field;
void doStuff() {}
};
struct D1 : public B {
D1(int field) : B{field} {}
bool operator==(const D1& d) {
return field == d.field;
}
};
struct D2 : public B {
D2(int field) : B{field} {}
bool operator==(const D2& d) {
return field == d.field;
}
};
或者,按照通常的偏好,您可以将它们设为免费函数:
bool operator==(const D1 &lhs, const D1 &rhs)
{
return lhs.field == rhs.field;
}
bool operator==(const D2 &lhs, const D2 &rhs)
{
return lhs.field == rhs.field;
}
注意:如果field
不是public成员,您需要将自由功能版本声明为friend
。
处理大量任意类型
好吧,也许您还有 D3
到 D99
,其中一些是 B
的间接后代。您需要使用模板:
template <class T>
bool operator==(const T &lhs, const T &rhs)
{
return lhs.field == rhs.field;
}
太棒了!但这抓住了 一切 ,这对于 不 应该具有可比性的不相关类型是不利的。所以我们需要约束。
TL;DR;解决方案
这是一个简单的实现,没有任何代码重复(即适用于任意数量的派生类型):
template <class T, class = std::enable_if<std::is_base_of<B,T>()
&& !std::is_same<B, std::remove_cv_t<std::remove_reference_t<T>>>()>>
bool operator==(const T &lhs, const T &rhs)
{
return lhs.field == rhs.field;
}
enable_if
首先检查 T
继承自 B
然后确保它 不是 B
。你在问题中说 B
基本上是一个抽象类型,从不直接实现,但它是一个编译时测试,所以为什么不偏执呢?
正如您后来在评论中指出的那样,并非所有 D#
都直接派生自 B
。 这仍然有效。
为什么你会遇到这个问题
鉴于以下情况:
D1 d1(1);
D2 d2(2);
d1 == d2;
编译器必须找到比较运算符,无论是自由函数还是 D1
的成员(不是 D2
)。值得庆幸的是,您已经在 class B
中定义了一个。上面的第三行可以等价地表述为:
d1.operator==(d2)
然而,operator==
是 B
的一部分,因此我们基本上调用 B::operator==(const B &)
。为什么当 D2
不是 B
时这会起作用?语言律师会澄清它在技术上是参数依赖查找 (ADL) 还是重载解析,但效果是 D2
作为函数调用的一部分静默转换为 B
,这相当于上面的:
d1.operator==(static_cast<B>(d2));
出现这种情况是因为找不到更好的比较函数。由于别无选择,编译器选择 B::operator==(const B &)
并进行转换。