检测结构体是否有填充。

20

有没有一种方法(trait或其他方式),可以检测结构体/类是否有填充?

我不需要跨平台或标准化的解决方案,我只需要针对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结构体,我就可以强制用户手动插入填充(例如某些控制填充而不是随机垃圾),这样就没有“随机”填充了。另一个好处是,当我比较两个相同场景的保存文件时,它们看起来是一样的。

编辑2 我越想越明白,我需要跨平台的解决方案。我们主要在msvc2013上开发,但我们的应用程序最终会在msvc2012和clang中进行构建。但是,如果我在msvc2013中检测并消除了所有由编译器生成的填充,那么就不能保证其他编译器不会插入填充...(因此,仅检测msvc2013是不够的)。


4
你为什么觉得你需要这个? - Lightness Races in Orbit
1
填充行为类似于未命名成员。由于您无法枚举成员,因此不可能区分普通成员和充当填充的那些“未命名成员”。 - MSalters
@JeroenBaert:这只是他试图检测的变异源之一。正如示例代码所示,它也可以按类别定义。 - MSalters
@LightnessRacesinOrbit 请查看编辑 - relaxxx
3
在很多情况下,你可以使用std::has_unique_object_representations。请参见编译时检查以确保结构体中没有任何填充 - phuclv
显示剩余3条评论
5个回答

4
在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++ 结构体内存对齐自动检查的方法。


2

您是否需要在运行时获取此信息?因为如果您想在构建时获取它,我相信您可以使用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);

同时也可以查看之前的帖子中提供的链接,或许会有所帮助。


5
谢谢,不幸的是我需要一种通用的解决方案。手动枚举所有结构体成员是不可能的。 - relaxxx
就像@relaxxx所说,我遇到了同样的问题,而且我没有看到一个简单的解决方案,除非枚举所有类型。你知道更优雅和通用的解决方案吗? - LXSoft
1
我认为这应该转换为静态常量布尔值has_padding =(sizeof(foo)!= EXPECTED_FOO_SIZE); 或者静态常量布尔值has_padding =!(sizeof(foo)== EXPECTED_FOO_SIZE); 只有在大小不同时才会存在填充。 对结果或测试进行否定对于上述变量的真正意义。 - LXSoft

2
从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;

...但它确实强制您修改底层类型(这就是为什么我仍然不知道一个完全通用的解决方案)。

至于您提问背后的动机,编辑朝着正确的方向前进。消息的序列化和反序列化是一个常见的问题,有许多框架和协议可以解决这个问题。它们通常涉及将数据安排在一个平台无关的格式中,然后可以由不同的平台解包,但选择协议是一个庞大的主题,超出了本回答的范围。然而,确定一个类型是否有填充字节有时会在生成带有随机数据的单元测试类型的上下文中出现,所以确定某物是否具有填充字节仍然是相关的。


1

试试这个宏:

#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 padding\n");
} else {
    printf("foo doesn't have padding\n");
}
if (has_padding_bar)
{
    printf("bar has padding\n");
} else {
    printf("bar has no padding\n");
}

当然,如果你想在编译时就出错,你可以使用 static_assert。


谢谢,但我不认为这会在不触及现有结构的情况下起作用,我需要“trait”。请看我的问题。 - relaxxx
好的,您不需要修改它的内容,也不需要更改它的包,但是您需要使用上面的宏来声明它。结构不会被改动。 - MichaelCMS
不触碰是指您无法修改声明结构的文件吗? - MichaelCMS
是的。我不想(也做不到)改变我们代码库中所有结构体,并强制其他人编写类似的结构体,请参阅http://www.cplusplus.com/reference/type_traits/以获取示例。 - relaxxx
那你做不了这件事。用TMP traits没有办法做到这一点。 - Lightness Races in Orbit

-1

也许你可以尝试这样做:

#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。


1
这并不考虑非填充空间(例如vPtr)并且需要手动枚举成员。 - Quentin
谢谢,不幸的是我需要通用解决方案,手动枚举所有结构体成员是不可能的。 - relaxxx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接