防止兄弟结构的平等比较

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

结构 D1D2 (以及更多类似的结构)派生自 B 以共享公共字段和方法,这样我就不需要在中复制这些字段和方法每个派生 类.

Struct B 从未被实例化;我只使用 D1D2 的实例。此外,D1D2 根本不应该相互交互。本质上,我不想要任何多态行为:D1D2,出于所有目的,应该充当不相关的结构。

我希望将任何 D1 与其他 D1 比较是否相等,将任何 D2 与其他 D2 进行比较是否相等。由于 D1D2 不包含任何字段,因此在结构 B.

中定义相等运算符似乎是合适的

但是,(如预期的那样)我在 D1D2 之间得到以下交互:

int main() {
    D1 d1a(1);
    D1 d1b(1);
    D2 d2(1);

    assert(d1a == d1b); // good

    assert(d1a == d2); // oh no, it compiles!
}

我不想将 D1D2 对象进行比较,因为出于所有目的,它们应该表现得好像它们是无关的。

如何在不复制代码的情况下使最后一个断言成为编译错误?为 D1D2(以及所有其他类似结构)分别定义相等运算符将意味着代码重复,因此我希望尽可能避免这种情况。

您可以使用 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。显然,D1D2 不应该共享它们的相等运算符,因为这两个运算符采用不同的参数。您当然可以将部分实现共享为 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

处理大量任意类型

好吧,也许您还有 D3D99,其中一些是 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 &) 并进行转换。