检测结构体是否有填充

Detect if struct has padding

本文关键字:填充 是否 结构体 检测      更新时间:2023-10-16

是否有一种方法(trait或左右)来检测,如果struct/class有一些填充?

我不需要跨平台或标准化的解决方案,我需要的是MSVC2013。

我可以输入

namespace A
{
    struct Foo
    {
        int a;
        bool b;
    };
}
#pragma pack(push, 1)
namespace B
{
    struct Foo
    {
        int a;
        bool b;
    };
}
#pragma pack(pop)
static const bool has_padding = sizeof(A::Foo) != sizeof(B::Foo);

但是c++不允许(据我所知)生成这种非侵入性(不触及现有结构)

理想情况下,我想开始这样的工作

template <typename T>
struct has_padding_impl
{
    typedef __declspec(align(1)) struct T AllignedT;
};
template <typename T>
struct has_padding : typename std::conditional<sizeof(typename has_padding_impl<T>::AllignedT) == sizeof(T),
                                               std::false_type,
                                               std::true_type>::type{};

编辑-为什么我需要这个?

我正在使用现有的序列化系统,该系统存储一些结构体,只是将void*传递给它们(在泛型函数内)并存储sizeof(T)的字节数…这样的二进制文件在我们的目标平台上是不可移植的,因为使用了不同的编译器,所以不能保证填充是如何插入的。如果我可以静态地检测所有T,这是填充结构,我可以强迫用户手动插入填充(一些控制填充,例如,不只是随机垃圾),所以没有"随机"填充。另一个优点是,当我对相同场景的两个保存文件进行diff时,它们看起来是一样的。

编辑2 我越想越意识到我需要跨平台的解决方案。我们主要在msvc2013上开发,但我们的应用程序最终在msvc2012和clang上构建。但是,如果我检测并消除了msvc2013中所有编译器生成的填充,则无法保证其他编译器不会插入填充……(所以msvc2013检测不够)

在c++ 17中最接近的是std::has_unique_object_representations。它基本上是您建议的has_padding<T>结构的布尔逆,但对于浮点类型也失败。

在我的代码库中,我使用std::has_unique_object_representations_v<T> || std::is_same_v<T, float> || std::is_same_v<T, double>的组合来检查我需要的内容,但是如果float是一个成员变量,例如,这将不包括。

你可能需要制作自己的has_padding实现结构,它具有遵循std::has_unique_object_representations<T>的通用实现,其中包含floatdouble成员的自定义类型可以在您手动检查后添加专门化:

#include <type_traits>
template <typename T>
struct has_padding : std::conditional<std::has_unique_object_representations<T>::value || std::is_same<T, float>::value || std::is_same<T, double>::value,
                                      std::false_type,
                                      std::true_type>::type{};
struct MyType
{
    float a;
    float b;
};
// I've checked it manually and decided MyType is okay:
template<>
struct has_padding<MyType> : std::false_type{};

这不是完美的,但它是最接近你会得到自动检查结构填充在c++中,据我所知。

在运行时需要这些信息吗?因为如果您想在构建时知道它,我相信您可以使用static_assert来获取此信息。

struct foo
{
    uint64_t x;
    uint8_t y;
};
#define EXPECTED_FOO_SIZE (sizeof(uint64_t) + sizeof(uint8_t))
static_assert(sizeof(foo) == EXPECTED_FOO_SIZE, "Using padding!");

如果您在运行时需要它,您可以尝试这样做:

static const bool has_padding = (sizeof(foo) != EXPECTED_FOO_SIZE);

也看看这个链接从以前的帖子,也许它会有所帮助。

试试这个宏:

#define TO_STR(str) #str
#define DECL_STRUCT_TEST_ALIGNED(structName, test_alignment, body) 
_Pragma(TO_STR(pack(push,test_alignment)))
struct test_##structName 
body ; 
_Pragma(TO_STR(pack(pop))) 
struct structName 
body; 
static const bool has_padding_##structName = sizeof(test_##structName)!=sizeof(structName);
DECL_STRUCT_TEST_ALIGNED(bar, 1,
{
                         int a;
                         bool b;
                     }
                     )

DECL_STRUCT_TEST_ALIGNED(foo,1,
{
                         int a;
                         int b;
                     })

现在,在运行时你可以测试:

if (has_padding_foo)
{
    printf("foo has paddingn");
} else {
    printf("foo doesn't have paddingn");
}
if (has_padding_bar)
{
    printf("bar has paddingn");
} else {
    printf("bar has no paddingn");
}

和ofc,你可以使用static_assert如果你想在编译时得到错误

在c++ 20中,最终(几乎)有足够的工具可以以通用的方式检测填充—至少对于普通的可复制对象是这样。一般思路是构建类型的对象表示,将其bit_cast<>()为感兴趣的类型,并将其与引用类型进行值比较。如果您打乱了对象表示中的每个字节,并且所有值比较返回不相等,则不应该有任何填充字节。

#include <array>
#include <bit>
#include <cstdint>
template <typename T>
consteval bool HasPadding() {
  auto bytes = std::array<uint8_t, sizeof(T)>{};
  const T reference = std::bit_cast<T>(std::array<uint8_t, sizeof(T)>{});
  for (uint32_t i = 0; i < sizeof(T); ++i) {
    bytes[i] = 1u;  // Perturb the object representation.
    const T instance = std::bit_cast<T>(bytes);
    if (instance == reference) {
      return true;
    }
    bytes[i] = 0u;  // Restore the object representation.
  }
  return false;
}

的问题是需要在感兴趣的类型中定义值比较的==操作符(或类似操作符)。这不是一个过分的要求;宇宙飞船操作员用一行字写了这句话:

auto operator<=>(const Type&) const = default;

…但它确实迫使您修改底层类型(这就是为什么我仍然没有意识到一个完全通用的解决方案)。

至于你的问题背后的动机,编辑的方向是正确的。消息的序列化和反序列化是一个常见的问题,有许多框架和协议可以解决这个问题。它们通常涉及以平台无关的格式安排数据,然后可以由不同的平台解压缩,但是选择协议是一个巨大的主题,超出了这个答案的范围。然而,一个类型是否有填充字节有时会在单元测试中使用随机数据生成类型的上下文中出现,因此确定是否有填充字节仍然是相关的。

也许你应该尝试这样做:

#include <iostream>
using namespace std;
struct A
{
    int a;
    bool b;
};
int main(int argc, char *argv[])
{
    A foo;
    cout << "sizeof struct = " << sizeof(A) << endl;
    cout << "sizeof items  = " << sizeof(foo.a) + sizeof(foo.b) << endl;
    return 0;
}
我:

sizeof struct = 8
sizeof items  = 5

我用的是Ubuntu 14.04