出于代码重用的目的,我有许多从同一基础派生的结构,但是我不希望有任何形式的多态性。
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
派生而来共享公共的字段和方法,因此我不需要在每个字段中重复这些字段和方法。派生类。
结构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
(以及所有其他类似的结构)定义相等运算符将意味着代码重复,因此,我希望避免这种情况。
答案 0 :(得分:7)
“结构D1和D2(以及更多类似的结构)从B派生来共享公共字段和方法”
然后将B
设为private
的基类。显然,D1
和D2
不应共享其相等运算符,因为这两个运算符采用不同的参数。您当然可以将bool B::equal(B const&) const
共享一部分实现,因为外部用户将无法访问。
答案 1 :(得分:4)
仅在引用最终类型的基类时,才可以使用CRTP来定义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
编译,而第二个被拒绝。
答案 2 :(得分:3)
通常,在派生类中需要两个函数来代替将相等运算符定义为基类的一部分:
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
不是公共成员,则需要将自由函数版本声明为friend
。
好的,也许您还有D3
至D99
,其中有些是B
的间接后代。您需要使用模板:
template <class T>
bool operator==(const T &lhs, const T &rhs)
{
return lhs.field == rhs.field;
}
太好了!但这会抢夺一切,这对于那些不是应该具有可比性的无关类型不利。所以我们需要约束。
这是一个没有任何代码重复的简单实现(即适用于任意数量的派生类型):
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
)。幸运的是,您已经在类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 &)
并进行强制转换。
答案 3 :(得分:2)
您可以从结构的原始定义中删除等式运算符,然后将其替换为接受两种相同参数类型的函数模板:
template <class T> bool operator == (const T& lhs, const T& rhs)
{
return lhs.field == rhs.field;
}
请注意,此函数有些“贪婪”,最好将其放在命名空间中(与结构一起使用,以启用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在评论中指出的)类型推导和对派生结构的基类引用,
B& b1 = d1a;
B& b2 = d2;
assert(b1 == b2); // Compiles, but see below.
您可能还想添加
template <> bool operator == <B>(const B&, const B&) = delete;