如何比较 C++ 中的泛型结构?
Posted
技术标签:
【中文标题】如何比较 C++ 中的泛型结构?【英文标题】:How to compare generic structs in C++? 【发布时间】:2020-05-21 14:45:57 【问题描述】:我想以一种通用的方式比较结构,我做了这样的事情(我不能分享实际的来源,所以如果需要,请询问更多细节):
template<typename Data>
bool structCmp(Data data1, Data data2)
void* dataStart1 = (std::uint8_t*)&data1;
void* dataStart2 = (std::uint8_t*)&data2;
return memcmp(dataStart1, dataStart2, sizeof(Data)) == 0;
这主要按预期工作,除了有时它返回 false 即使两个结构实例具有相同的成员(我已经用 eclipse 调试器检查过)。经过一番搜索,我发现memcmp
可能会因为使用的结构被填充而失败。
有没有更合适的方法来比较与填充无关的内存?我无法修改使用的结构(它们是我正在使用的 API 的一部分),并且使用的许多不同的结构有一些不同的成员,因此不能以通用的方式单独比较(据我所知)。
编辑:不幸的是,我被 C++11 困住了。早该提过这个...
【问题讨论】:
你能举一个失败的例子吗?一种类型的所有实例的填充应该相同,不是吗? @idclev463035818 填充未指定,你不能假设它的价值,我相信尝试阅读它是 UB(不确定最后一部分)。 @idclev463035818 填充在内存中的相同相对位置,但它可以有不同的数据。它在结构的正常使用中被丢弃,因此编译器可能不会费心将其归零。 @idclev463035818 填充具有相同的大小。构成该填充的位的状态可以是任何东西。当您memcmp
时,您会将这些填充位包含在比较中。
我同意 Yksisarvinen ... 使用类,而不是结构,并实现 ==
运算符。使用memcmp
是不可靠的,迟早你会处理一些必须“与其他人稍有不同”的类。在操作员中实现它非常干净和高效。实际行为将是多态的,但源代码将是干净的......而且,很明显。
【参考方案1】:
我相信您可以在 magic_get
库中基于 Antony Polukhin 的奇妙狡猾巫毒教解决方案 - 用于结构,而不是用于复杂类。
使用该库,我们可以在纯通用模板代码中迭代结构的不同字段及其适当的类型。例如,Antony 已经使用它来将任意结构流式传输到具有正确类型的输出流,完全通用。按理说,比较也可能是这种方法的一种可能应用。
... 但你需要 C++14。至少它比其他答案中的 C++17 和更高版本的建议要好:-P
【讨论】:
【参考方案2】:不,memcmp
不适合这样做。而 C++ 中的反射目前还不足以做到这一点(将会有实验性编译器支持足够强大的反射来做到这一点,c++23 可能具有您需要的功能)。
如果没有内置反射,解决问题的最简单方法是进行一些手动反射。
拿着这个:
struct some_struct
int x;
double d1, d2;
char c;
;
我们希望做最少的工作,以便我们可以比较其中的两个。
如果我们有:
auto as_tie(some_struct const& s)
return std::tie( s.x, s.d1, s.d2, s.c );
或
auto as_tie(some_struct const& s)
-> decltype(std::tie( s.x, s.d1, s.d2, s.c ))
return std::tie( s.x, s.d1, s.d2, s.c );
对于c++11,则:
template<class S>
bool are_equal( S const& lhs, S const& rhs )
return as_tie(lhs) == as_tie(rhs);
做得相当不错。
我们可以通过一些工作将这个过程扩展为递归;而不是比较关系,而是比较包装在模板中的每个元素,并且该模板的 operator==
递归地应用此规则(将元素包装在 as_tie
中以进行比较),除非该元素已经有一个工作 ==
,并处理数组。
这将需要一些库(100 行代码?)以及编写一些手动的每个成员“反射”数据。如果您拥有的结构数量有限,手动编写每个结构的代码可能会更容易。
大概有办法得到
REFLECT( some_struct, x, d1, d2, c )
使用可怕的宏生成as_tie
结构。但是as_tie
很简单。在c++11 中,重复很烦人;这很有用:
#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
return __VA_ARGS__;
在这种情况和许多其他情况下。使用RETURNS
,写as_tie
是:
auto as_tie(some_struct const& s)
RETURNS( std::tie( s.x, s.d1, s.d2, s.c ) )
删除重复。
这里是使其递归的尝试:
template<class T,
typename std::enable_if< !std::is_class<T>, bool>::type = true
>
auto refl_tie( T const& t )
RETURNS(std::tie(t))
template<class...Ts,
typename std::enable_if< (sizeof...(Ts) > 1), bool>::type = true
>
auto refl_tie( Ts const&... ts )
RETURNS(std::make_tuple(refl_tie(ts)...))
template<class T, std::size_t N>
auto refl_tie( T const(&t)[N] )
// lots of work in C++11 to support this case, todo.
// in C++17 I could just make a tie of each of the N elements of the array?
// in C++11 I might write a custom struct that supports an array
// reference/pointer of fixed size and implements =, ==, !=, <, etc.
struct foo
int x;
;
struct bar
foo f1, f2;
;
auto refl_tie( foo const& s )
RETURNS( refl_tie( s.x ) )
auto refl_tie( bar const& s )
RETURNS( refl_tie( s.f1, s.f2 ) )
c++17refl_tie(array)(完全递归,甚至支持arrays-of-arrays):
template<class T, std::size_t N, std::size_t...Is>
auto array_refl( T const(&t)[N], std::index_sequence<Is...> )
RETURNS( std::array<decltype( refl_tie(t[0]) ), N> refl_tie( t[Is] )... )
template<class T, std::size_t N>
auto refl_tie( T(&t)[N] )
RETURNS( array_refl( t, std::make_index_sequence<N> ) )
Live example.
这里我使用std::array
或refl_tie
。这比我之前编译时的 refl_tie 元组要快得多。
还有
template<class T,
typename std::enable_if< !std::is_class<T>, bool>::type = true
>
auto refl_tie( T const& t )
RETURNS(std::cref(t))
在这里使用std::cref
而不是std::tie
可以节省编译时开销,因为cref
是一个比tuple
简单得多的类。
最后,你应该添加
template<class T, std::size_t N, class...Ts>
auto refl_tie( T(&t)[N], Ts&&... ) = delete;
这将防止数组成员衰减为指针并退回到指针相等(您可能不希望数组中出现这种情况)。
如果没有这个,如果你将一个数组传递给一个非反射结构,它会退回到指向非反射结构 refl_tie
的指针,它可以工作并返回废话。
这样会导致编译时错误。
通过库类型支持递归是很棘手的。你可以std::tie
他们:
template<class T, class A>
auto refl_tie( std::vector<T, A> const& v )
RETURNS( std::tie(v) )
但这不支持通过它进行递归。
【讨论】:
我想通过手动反射来寻求这种类型的解决方案。您提供的代码似乎不适用于 C++11。你有没有机会帮我解决这个问题? 这在 C++11 中不起作用的原因是as_tie
缺少尾随返回类型。从 C++14 开始,这是自动推导出来的。您可以在 C++11 中使用auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));
。或者明确声明返回类型。
@FredrikEnetorp 已修复,外加一个易于编写的宏。使其完全递归工作的工作(因此结构结构,其中子结构具有as_tie
支持,自动工作)和支持数组成员的工作没有详细说明,但这是可能的。
谢谢。我做了可怕的宏略有不同,但功能相同。只是多了一个问题。我试图将比较概括在一个单独的头文件中,并将其包含在各种 gmock 测试文件中。这会导致错误消息:`as_tie(Test1 const&)' 的多个定义我正在尝试内联它们,但无法使其正常工作。
@FredrikEnetorp inline
关键字应该可以消除多个定义错误。收到minimal reproducible example后使用[提问]按钮【参考方案3】:
简而言之:不可能以通用方式。
memcmp
的问题是填充可能包含任意数据,因此memcmp
可能会失败。如果有办法找出填充的位置,您可以将这些位清零,然后比较数据表示,如果成员是平凡可比的,这将检查是否相等(这不是std::string
的情况,因为两个字符串可以包含不同的指针,但指向的两个字符数组是相等的)。但我知道没有办法获得结构的填充。您可以尝试告诉您的编译器打包结构,但这会使访问变慢并且不能真正保证工作。
实现这一点的最简洁方法是比较所有成员。当然,这在通用方式中是不可能的(直到我们在 C++23 或更高版本中获得编译时反射和元类)。从 C++20 开始,可以生成默认的 operator<=>
,但我认为这也只能作为成员函数,所以这又不是真的适用。如果你很幸运并且你想要比较的所有结构都定义了operator==
,你当然可以使用它。但这不能保证。
编辑: 好的,实际上有一个完全 hacky 且有点通用的聚合方式。 (我只写了到元组的转换,它们有一个默认的比较运算符)。 godbolt
【讨论】:
不错的技巧!不幸的是,我被 C++11 卡住了,所以我不能使用它。【参考方案4】:假设 POD 数据,默认赋值运算符仅复制成员字节。 (实际上不是 100% 确定这一点,不要相信我的话)
你可以利用它来发挥你的优势:
template<typename Data>
bool structCmp(Data data1, Data data2) // Data is POD
Data tmp;
memcpy(&tmp, &data1, sizeof(Data)); // copy data1 including padding
tmp = data2; // copy data2 only members
return memcmp(&tmp, &data1, sizeof(Data)) == 0;
【讨论】:
@walnut 没错,这是一个糟糕的答案。重写了一个。 标准是否保证分配不影响填充字节?对于基本类型中相同值的多个对象表示仍然存在问题。 @walnut 我相信it does. 该链接顶部答案下的 cmets 似乎表明它没有。答案本身只是说填充不需要被复制,而不是它不能。不过我也不确定。 我现在已经对其进行了测试,但它不起作用。赋值不会保留填充字节不变。【参考方案5】:C++ 20 支持default comaparisons
#include <iostream>
#include <compare>
struct XYZ
int x;
char y;
long z;
auto operator<=>(const XYZ&) const = default;
;
int main()
XYZ obj1 = 4,5,6;
XYZ obj2 = 4,5,6;
if (obj1 == obj2)
std::cout << "objects are identical\n";
else
std::cout << "objects are not identical\n";
return 0;
【讨论】:
虽然这是一个非常有用的功能,但它并没有回答所提出的问题。 OP 确实说“我无法修改使用的结构”,这意味着,即使 C++20 默认相等运算符可用,OP 也无法使用它们,因为默认==
或 @987654324 @ 运算符只能在类范围内完成。
就像 Nicol Bolas 说的,我不能修改结构。【参考方案6】:
您是对的,填充会妨碍您以这种方式比较任意类型。
您可以采取一些措施:
如果您控制Data
,那么例如gcc 拥有__attribute__((packed))
。它对性能有影响,但值得一试。不过,我不得不承认我不知道packed
是否能让你完全禁止填充。 Gcc doc 说:
如果您无法控制附加到结构或联合类型定义的此属性指定结构或联合的每个成员的放置以最小化所需的内存。当附加到枚举定义时,它表示应该使用最小的整数类型。
Data
,那么至少std::has_unique_object_representations<T>
可以告诉您您的比较是否会产生正确的结果:
如果 T 是 TriviallyCopyable 并且如果任何两个具有相同值的 T 类型对象具有相同的对象表示,则提供等于 true 的成员常量值。对于任何其他类型,值为 false。
还有:
引入此特征是为了可以通过将其对象表示散列为字节数组来确定类型是否可以正确散列。
PS:我只讨论了填充,但不要忘记,对于内存中具有不同表示的实例,可以比较相等的类型绝非罕见(例如std::string
、std::vector
和许多其他)。
【讨论】:
我喜欢这个答案。使用此类型特征,您可以使用 SFINAE 在没有填充的结构上使用memcmp
,并仅在需要时实现 operator==
。
好的,谢谢。有了这个,我可以安全地得出结论,我需要做一些手动反射。以上是关于如何比较 C++ 中的泛型结构?的主要内容,如果未能解决你的问题,请参考以下文章