代码之家  ›  专栏  ›  技术社区  ›  Bernard

防止同级结构的相等比较

  •  7
  • Bernard  · 技术社区  · 6 年前

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

    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 共享公共字段和方法,这样我就不需要在每个派生类中复制这些字段和方法。

    结构 从未实例化;我只使用 D1号 D2级 是的。此外, D1号 D2级 根本不应该互相影响。基本上,我不想有任何多态行为: D1号 D2级 ,在任何情况下,都应充当不相关的结构。

    我想要一些 D1号 与其他人相比 D1号 S代表平等, D2级 与其他人相比 D2级 代表平等。自从 D1号 D2级 不包含任何字段,在struct中定义相等运算符似乎是合适的 是的。

    然而,(正如预期的那样)我得到了 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级 (以及所有其他类似的结构)意味着代码重复,所以我希望尽可能避免这种情况。

    4 回复  |  直到 6 年前
        1
  •  4
  •   user4815162342    6 年前

    你可以用 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
  •  7
  •   MSalters    6 年前

    “结构d1和d2(以及更相似的结构)从b派生以共享公共字段和方法”

    那就做 B private 基类。显然, D1 D2 不应该共享它们的相等运算符,因为这两个运算符采用不同的参数。当然,您可以将实现的一部分共享为 bool B::equal(B const&) const ,因为外部用户无法访问。

        3
  •  3
  •   jonspaceharper    6 年前

    与将相等运算符定义为基类的一部分不同,派生类中通常需要两个函数:

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

    伟大的!但是这个抓住了 一切 ,这对于 应该是可比的。所以我们需要约束。

    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 继承自 然后确保 是的。你在问题中说 基本上是抽象类型,从来没有直接实现过,但它是编译时测试,所以为什么不偏执呢?

    正如你后来在评论中指出的,并非所有 D# 直接从 是的。 这仍然有效。

    为什么你会有这个问题

    鉴于以下情况:

    D1 d1(1);
    D2 d2(2);
    d1 == d2;
    

    编译器必须找到一个比较运算符,不管是自由函数还是 D1 ( D2 )中。谢天谢地,你在课堂上定义了一个 是的。上面的第三行可以等价地表示为:

    d1.operator==(d2)
    

    operator== 但是,它是 ,所以我们基本上是打电话 B::operator==(const B &) 是的。为什么在 D2级 不是 是吗?语言律师会澄清它是技术上依赖于参数的查找(adl)还是重载解析,但其结果是 D2级 是默默地投给 作为函数调用的一部分,使其等同于上述内容:

    d1.operator==(static_cast<B>(d2));
    

    这是因为找不到更好的比较函数。由于没有其他选择,编译器选择 B::运算符==(常量B&) 做演员。

        4
  •  2
  •   lubgr    6 年前

    可以从结构的原始定义中删除相等运算符,并将其替换为接受两个相同参数类型的函数模板:

    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;