memcmp 与多重相等性比较
memcmp vs multiple equality comparisons
先决条件:考虑这样一个class或结构T
,对于a
和b
类型的两个对象T
memcmp(&a, &b, sizeof(T)) == 0
产生与
相同的结果
a.member1 == b.member1 && a.member2 == b.member2 && ...
(memberN
是T
的非静态成员变量).
问题:什么时候应该使用memcmp
来比较a
和b
是否相等,什么时候应该使用链式[=24] =]被使用了吗?
这是一个简单的例子:
struct vector
{
int x, y;
};
要为 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
组件:
- 如果用
==
来实现operator==
,就得修改
- 如果使用
memcmp
,则 operator==
根本不需要修改。
但我认为使用链式 ==
s 传达了更清晰的含义。虽然对于成员众多的大型 T
memcmp
更具诱惑力。此外,与 ==
相比,使用 memcmp
是否会提高性能?还有什么要考虑的吗?
如果如您所说,您选择的类型使两个解决方案产生相同的结果(那么,大概您没有间接数据并且 alignment/padding 完全相同),那么显然您可以使用任何您喜欢的解决方案。
需要考虑的事项:
- 性能:我怀疑您是否会看到任何差异,但如果您关心的话,测量一下以确保;
- 安全: 你说这两个解决方案对你的
T
是一样的,但它们是吗?他们真的吗?在所有系统上?您的 memcmp
方法可移植吗?可能不是;
- 清晰度: 如果您的先决条件发生变化并且您没有充分评论描述您的
memcmp
用法,那么您的程序很可能会中断 - 因此您已经让它变得脆弱;
- 一致性: 想必你在别处使用了
==
;当然,对于每个 T
不满足先决条件的情况,您都必须这样做;除非这是针对 T
的有意优化专业化,否则您可以考虑在整个程序中坚持采用单一方法;
- 易用性:当然,很容易错过链式
==
中的成员,尤其是当您的成员列表不断增长时。
如果两个解决方案都正确,请选择更具可读性的解决方案。我会说对于 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); }
如果只有结构是 POD 并且它是安全的 memcmp
可比较(甚至不是所有数字类型都是...),结果是相同的,问题是关于可读性和性能。
可读性? 我认为这是一个相当基于意见的问题,但我更喜欢 operator==
.
性能? operator==
是短路运算符。您可以在此处更好地控制您的程序,因为您可以重新排序比较序列。
尽管 a == b && c == d
和 c == d && a == b
在算法逻辑方面是等效的(结果相同),但它们在生成的程序集、"background logic" 和可能的性能方面并不等效。
如果你能预见到一些点,你就可以影响你的程序。
例如:
- 如果两个语句产生错误的可能性大致相同,您将希望首先使用成本较低的语句,以尽可能跳过更复杂的比较。
- 如果两个陈述的复杂性大致相同,并且您事先知道
c == d
比 a == b
更可能为假,则您应该比较 c
和 d
第一.
可以使用 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==
并将其留给编译器考虑将比较优化为它认为更可取的任何程序集。
您施加了一个非常严格的条件,即没有填充(我假设 class 的成员之间和这些成员内部都没有)。我假设您还打算从 class 中排除任何 "hidden" 内务处理数据。此外,问题本身意味着我们总是比较完全相同类型的对象。在如此强大的条件下,可能无法提出一个反例,使基于 memcmp
的比较不同于 ==
比较。
出于性能原因,是否值得使用 memcmp
...好吧,如果您确实有充分的理由积极优化一些关键代码,并且分析表明从==
到memcmp
,那就一定要继续。但我不会将它用作编写比较运算符的常规技术,即使您的 class 满足要求。
==
比较好,因为memcmp
比较的是纯内存数据(这种比较在很多情况下是错误的,比如std::string
,array-imitiating 类或者即使它们不完全相同也可以相等的类型)。由于在你的 类 中可能有这样的类型,你应该始终使用它们自己的运算符而不是比较原始内存数据。
==
也更好,因为它比一些看起来很奇怪的函数更具可读性。
关于 memcmp
的前提条件与 ==
的成员比较产生相同的结果,虽然这个前提条件在实践中经常被满足,但它有点 脆弱 .
理论上,更改编译器或编译器选项可以打破该先决条件。更令人担忧的是,代码维护(所有编程工作的 80% 都是维护,IIRC)可以通过添加或删除成员、使 class 多态、添加自定义 ==
重载等来破坏它。作为其中一条评论中提到,前提条件可以适用于静态变量,但不适用于自动变量,然后创建非静态对象的维护工作可以做 Bad Things™。
以及关于是否使用memcmp
或成员==
为class实现==
运算符的问题,首先,这是一个错误二分法,因为这些不是唯一的选择。
例如,就 compare
函数而言,使用 自动生成关系运算符重载 可以减少工作量并且更易于维护。 std::string::compare
函数就是此类函数的一个示例。
其次,选择哪种实现的答案在很大程度上取决于您认为重要的内容,例如:
应该寻求最大化运行时效率,还是
应该寻求创建最清晰的代码,还是
应该寻求最简洁、最快编写代码,还是
应该设法使class最安全使用,还是
也许是别的东西?
正在生成关系运算符。
您可能听说过 CRTP,即 Curiously Recurring Template Pattern。我记得它是为了处理生成关系运算符重载的要求而发明的。不过,我可能会把它与其他东西混为一谈,但无论如何:
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:减法比较。
这是一个 class 提供了完整的关系运算符集而不使用 memcmp
或 ==
:
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
实现的 class 相同;我想您会同意此代码可伸缩性更好且更简单:
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
,直接提供并根据[实现=32=] 比较(使用 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 )
{}
};
如前所述,客户端代码使用例如>
需要 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 的格言:“过早的优化是万恶之源”(即花时间在这上面可能会适得其反)富有成效)。
先决条件:考虑这样一个class或结构T
,对于a
和b
类型的两个对象T
memcmp(&a, &b, sizeof(T)) == 0
产生与
相同的结果a.member1 == b.member1 && a.member2 == b.member2 && ...
(memberN
是T
的非静态成员变量).
问题:什么时候应该使用memcmp
来比较a
和b
是否相等,什么时候应该使用链式[=24] =]被使用了吗?
这是一个简单的例子:
struct vector
{
int x, y;
};
要为 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
组件:
- 如果用
==
来实现operator==
,就得修改 - 如果使用
memcmp
,则operator==
根本不需要修改。
但我认为使用链式 ==
s 传达了更清晰的含义。虽然对于成员众多的大型 T
memcmp
更具诱惑力。此外,与 ==
相比,使用 memcmp
是否会提高性能?还有什么要考虑的吗?
如果如您所说,您选择的类型使两个解决方案产生相同的结果(那么,大概您没有间接数据并且 alignment/padding 完全相同),那么显然您可以使用任何您喜欢的解决方案。
需要考虑的事项:
- 性能:我怀疑您是否会看到任何差异,但如果您关心的话,测量一下以确保;
- 安全: 你说这两个解决方案对你的
T
是一样的,但它们是吗?他们真的吗?在所有系统上?您的memcmp
方法可移植吗?可能不是; - 清晰度: 如果您的先决条件发生变化并且您没有充分评论描述您的
memcmp
用法,那么您的程序很可能会中断 - 因此您已经让它变得脆弱; - 一致性: 想必你在别处使用了
==
;当然,对于每个T
不满足先决条件的情况,您都必须这样做;除非这是针对T
的有意优化专业化,否则您可以考虑在整个程序中坚持采用单一方法; - 易用性:当然,很容易错过链式
==
中的成员,尤其是当您的成员列表不断增长时。
如果两个解决方案都正确,请选择更具可读性的解决方案。我会说对于 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); }
如果只有结构是 POD 并且它是安全的 memcmp
可比较(甚至不是所有数字类型都是...),结果是相同的,问题是关于可读性和性能。
可读性? 我认为这是一个相当基于意见的问题,但我更喜欢 operator==
.
性能? operator==
是短路运算符。您可以在此处更好地控制您的程序,因为您可以重新排序比较序列。
尽管 a == b && c == d
和 c == d && a == b
在算法逻辑方面是等效的(结果相同),但它们在生成的程序集、"background logic" 和可能的性能方面并不等效。
如果你能预见到一些点,你就可以影响你的程序。
例如:
- 如果两个语句产生错误的可能性大致相同,您将希望首先使用成本较低的语句,以尽可能跳过更复杂的比较。
- 如果两个陈述的复杂性大致相同,并且您事先知道
c == d
比a == b
更可能为假,则您应该比较c
和d
第一.
可以使用 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==
并将其留给编译器考虑将比较优化为它认为更可取的任何程序集。
您施加了一个非常严格的条件,即没有填充(我假设 class 的成员之间和这些成员内部都没有)。我假设您还打算从 class 中排除任何 "hidden" 内务处理数据。此外,问题本身意味着我们总是比较完全相同类型的对象。在如此强大的条件下,可能无法提出一个反例,使基于 memcmp
的比较不同于 ==
比较。
出于性能原因,是否值得使用 memcmp
...好吧,如果您确实有充分的理由积极优化一些关键代码,并且分析表明从==
到memcmp
,那就一定要继续。但我不会将它用作编写比较运算符的常规技术,即使您的 class 满足要求。
==
比较好,因为memcmp
比较的是纯内存数据(这种比较在很多情况下是错误的,比如std::string
,array-imitiating 类或者即使它们不完全相同也可以相等的类型)。由于在你的 类 中可能有这样的类型,你应该始终使用它们自己的运算符而不是比较原始内存数据。
==
也更好,因为它比一些看起来很奇怪的函数更具可读性。
关于 memcmp
的前提条件与 ==
的成员比较产生相同的结果,虽然这个前提条件在实践中经常被满足,但它有点 脆弱 .
理论上,更改编译器或编译器选项可以打破该先决条件。更令人担忧的是,代码维护(所有编程工作的 80% 都是维护,IIRC)可以通过添加或删除成员、使 class 多态、添加自定义 ==
重载等来破坏它。作为其中一条评论中提到,前提条件可以适用于静态变量,但不适用于自动变量,然后创建非静态对象的维护工作可以做 Bad Things™。
以及关于是否使用memcmp
或成员==
为class实现==
运算符的问题,首先,这是一个错误二分法,因为这些不是唯一的选择。
例如,就 compare
函数而言,使用 自动生成关系运算符重载 可以减少工作量并且更易于维护。 std::string::compare
函数就是此类函数的一个示例。
其次,选择哪种实现的答案在很大程度上取决于您认为重要的内容,例如:
应该寻求最大化运行时效率,还是
应该寻求创建最清晰的代码,还是
应该寻求最简洁、最快编写代码,还是
应该设法使class最安全使用,还是
也许是别的东西?
正在生成关系运算符。
您可能听说过 CRTP,即 Curiously Recurring Template Pattern。我记得它是为了处理生成关系运算符重载的要求而发明的。不过,我可能会把它与其他东西混为一谈,但无论如何:
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:减法比较。
这是一个 class 提供了完整的关系运算符集而不使用 memcmp
或 ==
:
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
实现的 class 相同;我想您会同意此代码可伸缩性更好且更简单:
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
,直接提供并根据[实现=32=] 比较(使用 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 )
{}
};
如前所述,客户端代码使用例如>
需要 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 的格言:“过早的优化是万恶之源”(即花时间在这上面可能会适得其反)富有成效)。