防止同级结构的相等比较

时间:2018-07-16 12:54:18

标签: c++ inheritance c++17 siblings

出于代码重用的目的,我有许多从同一基础派生的结构,但是我不希望有任何形式的多态性。

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派生而来共享公共的字段和方法,因此我不需要在每个字段中重复这些字段和方法。派生类。

结构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(以及所有其他类似的结构)定义相等运算符将意味着代码重复,因此,我希望避免这种情况。

4 个答案:

答案 0 :(得分:7)

“结构D1和D2(以及更多类似的结构)从B派生来共享公共字段和方法”

然后将B设为private的基类。显然,D1D2不应共享其相等运算符,因为这两个运算符采用不同的参数。您当然可以将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

处理大量任意类型

好的,也许您还有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)。幸运的是,您已经在类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;