24

前提条件:考虑这样一个类或结构T,对于两个对象ab类型T

memcmp(&a, &b, sizeof(T)) == 0

产生相同的结果

a.member1 == b.member1 && a.member2 == b.member2 && ...

(memberN是 ) 的非静态成员变量T

问题:什么时候应该memcmp使用比较ab相等,什么时候应该使用链式==s?


这是一个简单的例子:

struct vector
{
    int x, y;
};

要重载运算符==for vector,有两种可能性(如果保证它们给出相同的结果):

bool operator==(vector lhs, vector rhs)
{ return lhs.x == rhs.x && lhs.y == rhs.y; }

或者

bool operator==(vector lhs, vector rhs)
{ return memcmp(&lhs, &rhs, sizeof(vector)) == 0; }

现在,如果要添加一个新成员vector,例如一个z组件:

  • 如果==使用 s 来实现operator==,则必须对其进行修改。
  • 如果memcmp改为使用,则operator==根本不必修改。

但我认为使用 chained ==s 传达了更清晰的含义。虽然对于一个T拥有许多成员的大型memcmp来说更具诱惑力。此外,使用memcmpover ==s 是否会提高性能?还有什么要考虑的吗?

4

6 回答 6

17

关于与memcmp进行成员比较产生相同结果的前提条件==,虽然在实践中经常满足这个前提条件,但它有点脆弱

从理论上讲,更改编译器或编译器选项可以打破该先决条件。更令人担忧的是,代码维护(所有编程工作的 80% 是维护,IIRC)可以通过添加或删除成员、使类多态、添加自定义==重载等来破坏它。正如其中一条评论中提到的,前提条件可以适用于静态变量,但不能适用于自动变量,然后创建非静态对象的维护工作可能会做坏事™。

关于是使用memcmp还是按成员方式==为类实现==运算符的问题,首先,这是一种错误的二分法,因为这些不是唯一的选择。

例如,就函数而言,使用自动生成关系运算符重载compare可以减少工作量并且更易于维护。该std::string::compare函数是这种函数的一个示例。

其次,选择什么实现的答案很大程度上取决于您认为重要的内容,例如:

  • 是否应该寻求最大化运行时效率,或者

  • 是否应该寻求创建最清晰的代码,或者

  • 应该寻求最简洁、最快的代码编写方式,还是

  • 是否应该寻求使该类最安全地使用,或者

  • 别的东西,也许?

生成关系运算符。

您可能听说过 CRTP,即奇怪重复的模板模式。我记得它是为了处理生成关系运算符重载的要求而发明的。不过,我可能会将其与其他内容混为一谈,但无论如何:

template< class Derived >
struct Relops_from_compare
{
    friend
    auto operator!=( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) != 0; }

    friend
    auto operator<( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) < 0; }

    friend
    auto operator<=( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) <= 0; }

    friend
    auto operator==( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) == 0; }

    friend
    auto operator>=( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) >= 0; }

    friend
    auto operator>( const Derived& a, const Derived& b )
        -> bool
    { return compare( a, b ) > 0; }
};

鉴于上述支持,我们可以调查您的问题可用的选项。

实现A:减法比较。

这是一个提供全套关系运算符的类,而无需使用memcmpor ==

struct Vector
    : Relops_from_compare< Vector >
{
    int x, y, z;

    // This implementation assumes no overflow occurs.
    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        if( const auto r = a.x - b.x ) { return r; }
        if( const auto r = a.y - b.y ) { return r; }
        return a.z - b.z;
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

实现 B:比较通过memcmp.

这是使用memcmp;实现的同一个类。我想你会同意这段代码可以更好地扩展并且更简单:

struct Vector
    : Relops_from_compare< Vector >
{
    int x, y, z;

    // This implementation requires that there is no padding.
    // Also, it doesn't deal with negative numbers for < or >.
    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        static_assert( sizeof( Vector ) == 3*sizeof( x ), "!" );
        return memcmp( &a, &b, sizeof( Vector ) );
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

实现 C:逐个成员比较。

这是一个使用成员比较的实现。它没有强加任何特殊要求或假设。但它更多的是源代码。

struct Vector
    : Relops_from_compare< Vector >
{
    int x, y, z;

    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        if( a.x < b.x ) { return -1; }
        if( a.x > b.x ) { return +1; }
        if( a.y < b.y ) { return -1; }
        if( a.y > b.y ) { return +1; }
        if( a.z < b.z ) { return -1; }
        if( a.z > b.z ) { return +1; }
        return 0;
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

实现 D:compare就关系运算符而言。

这是一种颠倒事物自然顺序的实现方式,通过compare<实现==,直接提供并根据std::tuple比较(使用std::tie)实现。

struct Vector
{
    int x, y, z;

    friend
    auto operator<( const Vector& a, const Vector& b )
        -> bool
    {
        using std::tie;
        return tie( a.x, a.y, a.z ) < tie( b.x, b.y, b.z );
    }

    friend
    auto operator==( const Vector& a, const Vector& b )
        -> bool
    {
        using std::tie;
        return tie( a.x, a.y, a.z ) == tie( b.x, b.y, b.z );
    }

    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        return (a < b? -1 : a == b? 0 : +1);
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

如给定的,使用 eg 的客户端代码>需要一个using namespace std::rel_ops;.

替代方法包括将所有其他运算符添加到上述(更多代码),或使用 CRTP 运算符生成方案,该方案根据<=(可能效率低下)实现其他运算符。

实现 E:通过手动使用<和进行比较==

这个实现是没有应用任何抽象的结果,只是敲打键盘并直接写下机器应该做什么:

struct Vector
{
    int x, y, z;

    friend
    auto operator<( const Vector& a, const Vector& b )
        -> bool
    {
        return (
            a.x < b.x ||
            a.x == b.x && (
                a.y < b.y ||
                a.y == b.y && (
                    a.z < b.z
                    )
                )
            );
    }

    friend
    auto operator==( const Vector& a, const Vector& b )
        -> bool
    {
        return
            a.x == b.x &&
            a.y == b.y &&
            a.z == b.z;
    }

    friend
    auto compare( const Vector& a, const Vector& b )
        -> int
    {
        return (a < b? -1 : a == b? 0 : +1);
    }

    Vector( const int _x, const int _y, const int _z )
        : x( _x ), y( _y ), z( _z )
    {}
};

选择什么。

考虑最有价值的可能方面的列表,例如安全性、清晰度、效率、简短性,评估上述每种方法。

然后选择对你来说显然是最好的一种,或者看起来同样最好的方法之一。

指导:为了安全起见,您不想选择方法 A,减法,因为它依赖于对值的假设。请注意,选项 B, , 作为一般情况的实现也是不安全的,但对于和memcmp可以做得很好。为了提高效率,您应该更好地MEASURE,使用相关的编译器选项和环境,并记住 Donald Knuth 的格言:“过早的优化是万恶之源”(即花时间在上面可能会适得其反)。==!=

于 2015-03-04T17:48:55.357 回答
12

如果,如您所说,您选择的类型使得两种解决方案产生相同的结果(假设您没有间接数据并且对齐/填充都是相同的),那么显然您可以使用您喜欢的任何解决方案.

需要考虑的事项:

  1. 性能:我怀疑你会看到很多差异,但如果你在乎的,可以确定一下;
  2. 安全:嗯,你说这两种解决方案对你来说是一样的T,但是它们是吗?他们真的吗?在所有系统上?你的memcmp方法是可移植的吗?可能不是;
  3. 清晰性:如果您的先决条件发生了变化,并且您没有充分评论 - 描述您的memcmp用法,那么您的程序很可能会崩溃 - 因此您使其变得脆弱;
  4. 一致性:想必你==在别处使用;当然,你必须为每一个T不符合你的先决条件的人做这件事;除非这是对 的故意优化专业化T,否则您可以考虑在整个程序中坚持单一方法;
  5. 易于使用:当然,从 chained 中漏掉一个成员是很容易的==,特别是如果您的成员列表不断增长。
于 2015-03-04T15:38:28.327 回答
6

如果两个解决方案都是正确的,请选择更易读的一个。我想说,对于 C++ 程序员来说,==它比memcmp. 我什至会使用std::tie而不是链接:

bool operator==(const vector &lhs, const vector &rhs)
{ return std::tie(lhs.x, lhs.y) == std::tie(rhs.x, rhs.y); }
于 2015-03-04T15:40:25.243 回答
4

如果有的话,只有当结构是 POD 并且它是安全memcmp可比的(甚至不是所有的数字类型都是......)时,结果是相同的,问题是关于可读性和性能。

可读性?我认为这是一个相当基于意见的问题,但我更喜欢operator==.

表现? operator==是短路算子。您可以在此处对您的程序进行更多控制,因为您可以重新排序比较序列。

尽管a == b && c == dc == d && a == b在算法逻辑方面是等效的(结果是相同的),但它们在生成的程序集、“背景逻辑”和可能的性能方面并不等效。

如果你能预见一些观点,你就可以影响你的程序。

例如:

  • 如果两个语句产生错误的可能性大致相同,那么您将希望首先使用更便宜的语句来跳过更复杂的比较(如果可能)。
  • 如果两个陈述大致同样复杂,并且您事先知道它c == d比 更有可能是错误的a == b,您应该先比较cd

可以使用 while 以取决于问题的方式调整比较顺序,operator==而不memcmp会给您这种自由。

PS:您可能想要测量它,但对于具有 3 个成员的小型结构,MS VS 2013 会为该memcmp案例生成稍微复杂的装配。在这种情况下,我希望operator==解决方案具有更高的性能(如果影响是可测量的)。

-/编辑-

注意:即使是 POD 结构成员也可以重载operator==.

考虑:

#include <iostream>
#include <iomanip>

struct A { int * p; };

bool operator== (A const &a, A const &b) { return *(a.p) == *(b.p); }

struct B { A m; };

bool operator== (B const &a, B const &b) { return a.m == b.m; }

int main()
{
  int a(1), b(1);
  B x, y;
  x.m.p = &a;
  y.m.p = &b;
  std::cout << std::boolalpha;
  std::cout << (memcmp(&x, &y, sizeof(B)) == 0) << "\n";
  std::cout << (x == y) << "\n";
  return 0;
}

印刷

false
true

即使 - 反过来 - 所有成员都是我更喜欢的基本类型,operator==并将其留给编译器考虑将比较优化为它认为更可取的任何程序集。

于 2015-03-04T15:45:03.490 回答
1

您强加了一个非常严格的条件,即没有填充(我假设既不在班级成员之间,也不在这些成员内部)。我认为您还打算从课程中排除任何“隐藏”的家务数据。此外,问题本身意味着我们总是比较完全相同类型的对象。在如此强大的条件下,可能没有办法想出一个反例来使memcmp基于 - 的比较与比较不同==

出于性能原因是否值得使用memcmp......好吧,如果你真的有充分的理由积极优化一些关键的代码片段并且分析表明从切换==到之后有改进memcmp,那么一定要继续。但是,即使您的课程满足要求,我也不会将其用作编写比较运算符的常规技术。

于 2015-03-04T15:50:36.473 回答
1

==更好,因为memcmp比较纯内存数据(在许多情况下比较这种方式可能是错误的,例如std::string,即使它们不完全相同也可以相等的数组模仿类或类型)。由于在您的类中可能存在此类类型,因此您应该始终使用它们自己的运算符,而不是比较原始内存数据。

==也更好,因为它比一些看起来很奇怪的函数更具可读性。

于 2015-03-04T15:56:48.927 回答