有没有办法检查 2 个聚合是否等效?

Is there a way to check if 2 aggregates are equivalent?

本文关键字:是否 检查 有没有      更新时间:2023-10-16

假设我有 3 个聚合:

struct User
{
int age {};
std::string name;
std::string address;
};
struct Car
{
int power {};
std::string name;
std::string owner;
};
struct Student
{
std::string name;
std::string address;
std::int age {};
};

我会定义 2 个聚合是等效的,如果它们具有相同数量、相同类型和相同顺序的成员字段。

例如,UserCar是等效的:3 个字段:int, string, string,但UserStudent不是:一个是int, string, string,另一个是string, string, int

我想说这个功能的含义是显而易见的,您将能够非常轻松地复制 2 个不相关但相似的聚合。

编辑:聚合来自不同的地方,我无法更改它们,或者使它们从同一类继承,或者其他任何东西。我很感兴趣,如果所有 C++11/17 泛型、类型特征、SFINAE 魔法等,这完全是可能的。

编辑2:我刚刚发现了std::is_layout_compatible()它可能符合我的想法,但它计划在C++20发布。

结构等效性(忽略语义等效性(不是在没有编译器支持的情况下可以检查的功能。

您需要某种类型的基本反射,或者预先打包的东西喜欢 C++20std::is_layout_compatible.

您的特定情况很有趣,因为所有成员都是公共的,允许您使用 C++17 结构化绑定来获取对成员的引用,尽管您必须知道元素的数量以及它们是否是引用。

template <class T, class T2, class U, class U2>
bool structurally_equivalent_helper(T&& a, T2&& a2, U&& b, U2&& b2) {
return std::is_same_v<decltype(a2), decltype(b2)>
&& ((char*)&a2 - (char*)&a) == ((char*)&b2 - (char*)b);
}
template <class T, class U>
bool structurally_equivalent3(T&& a, U&& b) {
auto&& [a1, a2, a3] = std::forward<T>(a);
auto&& [b1, b2, b3] = std::forward<U>(b);
return structurally_equivalent_helper(a, decltype(a1)(a1), b, decltype(b1)(b1))
&& structurally_equivalent_helper(a, decltype(a2)(a2), b, decltype(b2)(b2))
&& structurally_equivalent_helper(a, decltype(a3)(a3), b, decltype(b3)(b3));
}

要在没有任何额外样板的情况下做到这一点,您需要反思,不幸的是,这还没有C++(不过,它可能会达到 C++23(。

您可以通过向每个对象添加tie函数来获得所需的大部分内容。

#include <tuple>
#include <string>
struct User
{
int age{};
std::string name;
std::string address;
auto tie() { return std::tie(age, name, address); }
};
struct Car
{
int power{};
std::string name;
std::string owner;
auto tie() { return std::tie(power, name, owner); }
};
struct Student
{
std::string name;
std::string address;
int age{};
auto tie() { return std::tie(name, address, age); }
};
int main() {
auto b1 = User().tie() == Car().tie();
auto b2 = User().tie() == Student().tie(); // compile error
}

magic_get库使它相对容易:

#include <cstddef>
#include <type_traits>
#include <utility>
#include <boost/pfr.hpp>
template <std::size_t ...I, typename F>
constexpr bool all_of_seq(std::index_sequence<I...>, F func)
{
return ((func(std::integral_constant<std::size_t, I>{})) && ...);
}
template <typename A, typename B>
inline constexpr bool is_equivalent_v = []
{
namespace pfr = boost::pfr;
if constexpr (!(sizeof(A) == sizeof(B) && pfr::tuple_size_v<A> == pfr::tuple_size_v<B>))
{
return false;
}
else
{
return all_of_seq(std::make_index_sequence<pfr::tuple_size_v<A>>{}, [&](auto index)
{
constexpr int i = index.value;
return std::is_same_v<pfr::tuple_element_t<i, A>, pfr::tuple_element_t<i, B>>;
});
}
}();

首先,我们确保两个结构具有相同的大小和相同的字段数量:

sizeof(A) == sizeof(B) && pfr::tuple_size_v<A> == pfr::tuple_size_v<B>

然后,我们比较字段类型:

std::is_same_v<pfr::tuple_element_t<i, A>, pfr::tuple_element_t<i, B>>

此解决方案主要等同于@Deduplicator建议的解决方案,但由于magic_get,它不涉及编写样板模板。

此外,此实现不比较字段偏移量(因为我认为它不能在编译时完成(,这使得它不太可靠:如果结构字段上有alignas,您可能会得到误报。

用法:

#include <iostream>
struct User
{
int age {};
std::string name;
std::string address;
};
struct Car
{
int power {};
std::string name;
std::string owner;
};
struct Foo
{
int x, y;
};
int main()
{
std::cout << is_equivalent_v<User, User> << 'n'; // 1
std::cout << is_equivalent_v<User, Car > << 'n'; // 1
std::cout << is_equivalent_v<User, Foo > << 'n'; // 0
std::cout << is_equivalent_v<Car , Foo > << 'n'; // 0
}

如果您事先知道类型,简单的解决方案是提供您正在使用的所有等效类型的描述。例如:

#include <iostream>
#include <type_traits>
using namespace std;
template<typename... Args>
struct EquivalenceType
{};
template<typename T>
struct EquivalenceClass
{};
template<typename T1, typename T2>
bool AreClassEquivalent()
{
return std::is_same<typename EquivalenceClass<T1>::Type, typename EquivalenceClass<T2>::Type>::value;
}
struct User
{
int age{};
std::string name;
std::string address;
};
struct Car
{
int power{};
std::string name;
std::string owner;
};
template<>
struct EquivalenceClass<User>
{
using Type = EquivalenceType<int, std::string, std::string>; 
};
template<>
struct EquivalenceClass<Car>
{
using Type = EquivalenceType<int, std::string, std::string>; 
};
int main()
{
cout << AreClassEquivalent<User, Car>() << endl;
}

对于您希望它"等效可比"的每个类,您需要提供等效类模板的专业化。

这样做的一大缺点是保持一致性,例如,在修改用户定义并忘记更新其 EquivalenceClass 之后。