如何将类的大小控制为成员大小的倍数?

How to control the size of a class to be a multiple of the size of a member?

提问人:alfC 提问时间:11/7/2023 最后编辑:alfC 更新时间:11/17/2023 访问量:356

问:

我有以下课程,

struct employee {
    std::string name;
    short salary;
    std::size_t age;
};

举个例子,在 Linux amd64 中,struct 的大小是 48 字节,std::string 的大小是 32,即不是倍数。

现在,我需要以跨平台的方式,使大小是(第一个成员)大小的倍数。employeestd::string

(例如,跨平台可能意味着 Linux amd64 和 Apple ARM。

那是。sizeof(employee) % sizeof(std::string) == 0

我尝试控制整个班级或成员的填充使用,但似乎要求是 2 的幂太严格了。alignas

然后我尝试在末尾添加一个数组。 尽管如此,我还是遇到了两个问题,首先,在编译时不同平台中数组的确切大小是多少,其次,没有添加另一个成员来搞砸类的良好聚合初始化。char

首先,我这样做:

struct employee_dummy {
    std::string name;
    short salary;
    std::size_t age;
};

struct employee {
    std::string name;
    short salary;
    std::size_t age;
    char padding[(sizeof(employee_dummy)/sizeof(std::string)+1)*sizeof(std::string) - sizeof(employee_dummy)];
};

注意丑陋的虚拟类,我什至不知道逻辑是否正确。

对于第二个问题,我没有解决方案。 我可以这样做,但是我需要添加一个构造函数,该类将不是聚合,等等。

struct employee {
    std::string name;
    short salary;
    std::size_t age;
 private:
    char padding[(sizeof(employee_dummy)/sizeof(std::string)+1)*sizeof(std::string) - sizeof(employee_dummy)];
};

如何使用标准或非标准机制控制结构的大小,并将类保留为聚合?

以下是根据经验解决这个问题的链接:https://cppinsights.io/s/f2fb5239


添加的注释:

我意识到,如果添加填充的技术是正确的,那么计算就更加困难了,因为虚拟类可能已经在添加填充,所以我必须考虑最后一个元素的偏移量。

在此示例中,我想成为第一个成员 () 的倍数:datastd::complex

struct dummy {
    std::complex<double> a;
    double b;
    std::int64_t b2;
    int c;
};

struct data {
    std::complex<double> a;
    double b;
    std::int64_t b2;
    int c;
    char padding[ ((offsetof(dummy, c) + sizeof(c)) / sizeof(std::complex<double>) + 1)* sizeof(std::complex<double>) - (offsetof(dummy, c) + sizeof(c)) ];
};

请注意,现在的公式更糟。

C++ 结构 对齐填充 大小

评论

1赞 Pepijn Kramer 11/7/2023
你为什么要这样做?是否计划创建序列化方案?
2赞 alfC 11/7/2023
@HolyBlackCat,我经常收到这个问题;不,它适用于整个框架,该框架假设步幅是元素大小的倍数。事实是,传统的 C 库,数字库和其他库,fortran 等,都使用元素大小的步幅。我认为,它还简化了指针算术。这是一个更数字的例子(我也意识到我也应该研究偏移量)cppinsights.io/s/971006bf
1赞 alfC 11/7/2023
@HolyBlackCat,如果您有兴趣,这是碰巧在 Linux amd64 上工作的原始示例,但最近我意识到它不适用于 Apple ARM:gitlab.com/correaa/boost-multi/-/blob/master/test/......
1赞 Botje 11/7/2023
您是否知道 的大小取决于所使用的标准库和编译器?它不仅仅是“amd64 linux”,而是“在调试模式下带有 libstdc++ 的 amd64 linux”std::string
1赞 alfC 11/8/2023
@DanielLangr 我认为如果成员之间存在间隙,该函数将不会提供正确的填充。

答:

10赞 n. m. could be an AI 11/9/2023 #1

这是一个符合标准的版本,没有如果或但是。

template <template<std::size_t> class tmpl, std::size_t need_multiple_of>
struct adjust_padding
{
    template <std::size_t n>
    static constexpr std::size_t padding_size()
    {
        if constexpr (sizeof(tmpl<n>) % need_multiple_of == 0) return n;
        else return padding_size<n+1>();
    }

    using type = tmpl<padding_size<0>()>;
};

像这样使用它:

template <std::size_t K>
struct need_strided
{
    double x;
    const char pad[K];
};

template <>
struct need_strided<0>
{
    double x;
};

using strided = adjust_padding<need_strided, 47>::type;

现在的大小是 47 的倍数(当然正确对齐)。在我的电脑上是 376。strided

您可以按以下方式制作模板:employee

template <std::size_t K>
struct employee { ...

或使其成为模板的成员(而不是):double x

template <std::size_t K>
struct employee_wrapper { 
   employee e;
   

然后用作矢量元素。但无论哪种方式,都为 0 提供专业化。employee_wrapper

您可以尝试使用 C 样式数组代替 C 样式数组,并避免为 0 提供专用化,但当大小为 0 时,它可能会也可能不会被优化。 (C++20)可能会有所帮助。std::array[[no_unique_address]]

请注意,类似的东西可能会溢出编译器的默认 constexpr 深度。adjust_padding<need_strided, 117>::type

评论

0赞 alfC 11/15/2023
这个想法几乎是万无一失的。递归可能表明存在更大的问题,例如需要太多的端部填充。不过有几处修改:1.使 non-const,否则默认赋值不起作用。2. 受益于非标准的零大小数组,不确定这是建议还是强制性的。3. 我用 , 4.否则我必须初始化,默认 operator== 可能不会产生正确的结果,我将被迫实现 operator==。这里的其他细节和具体用途:godbolt.org/z/K3Y61eY5bpad_[[no_unique_address]]char[K]std::array<char, K>pad_
0赞 n. m. could be an AI 11/15/2023
@alfC 你是对的,它不应该是常量。
4赞 VonC 11/9/2023 #2

鉴于注释提供的上下文,OP 的主要目标是创建一个大小为 大小的倍数的结构,以便出于遗留兼容性原因将此类结构数组视为具有一定步幅的数组。std::stringstd::string

C++ 标准没有以这样一种方式定义内存的布局,即您可以安全地跨过对象数组,就好像它是其成员之一的数组一样。
这是由于填充、对齐要求以及访问越界内存或创建不指向对象开头的指针时的潜在未定义行为所致。

我尝试了这个JDoodle SizeAdjuster项目作为更简单的方法:

#include <iostream>
#include <string>
#include <type_traits>

// Original employee struct with `salary` as an int
struct employee {
    std::string name;
    int salary;  // Changed from short to int
    std::size_t age;
};


// SizeAdjuster to make the size of a struct a multiple of the size of a member
template <typename T, typename MemberT>
struct SizeAdjuster {
    T data;
    // Calculate padding needed to make the size of T a multiple of the size of MemberT.
    char padding[(-sizeof(T)) % sizeof(MemberT)];
};

// Adjusted employee struct with padding
using employee_adjusted = SizeAdjuster<employee, std::string>;

int main() {
    // Create an employee object with adjusted size, salary is now an int, so 50000 is valid.
    employee_adjusted emp_adj = {{"John Doe", 50000, 30}};

    // Access and display employee data
    std::cout << "Name: " << emp_adj.data.name << std::endl;
    std::cout << "Salary: " << emp_adj.data.salary << std::endl;
    std::cout << "Age: " << emp_adj.data.age << std::endl;

    // Display the size of the adjusted employee object
    std::cout << "Size of employee: " << sizeof(employee) << std::endl;
    std::cout << "Size of std::string: " << sizeof(std::string) << std::endl;
    std::cout << "Size of employee_adjusted: " << sizeof(employee_adjusted) << std::endl;

    // Check if the size of employee_adjusted is a multiple of the size of std::string
    std::cout << "Is size of employee_adjusted a multiple of the size of std::string? "
              << (sizeof(employee_adjusted) % sizeof(std::string) == 0 ? "Yes" : "No") << std::endl;

    return 0;
}

这将考虑到零大小数组的问题,并试图通过确保我们始终有一个定义良好的数组来避免未定义的行为。但是,即使进行了此调整,在对象数组中大步前进的行为(就好像它是对象数组一样)仍可能调用未定义的行为。paddingemployee_adjustedstd::string

的目标是确保结构的大小是 的倍数。在给定的输出中:SizeAdjusteremployee_adjustedstd::string

Size of employee: 48
Size of std::string: 32
Size of employee_adjusted: 64
Is size of employee_adjusted a multiple of the size of std::string? Yes

这表示原始结构的大小为 bytes,大小为 bytes。
通过向结构体添加足够的填充来完成其工作,使总大小成为 的倍数,在本例中为 字节。由于是 的倍数,条件为真,这就是我们想要实现的。
employee48std::string32SizeAdjusteremployeestd::string646432sizeof(employee_adjusted) % sizeof(std::string) == 0

使大小的大小为 大小的倍数的目的是以这样一种方式对齐内存布局,即当您有一个对象数组时,理论上可以以一致的步幅直接访问该成员。在 OP 的上下文中,这是为了与期望这种内存对齐的遗留系统或框架兼容。employee_adjustedstd::stringemployee_adjustedname

但是,需要重申的是,虽然我们可以确保结构体的大小是 的倍数,但访问内存就像是数组一样,并不能保证安全或符合标准。该标准不支持将数组 视为数组,因为存在潜在的严格别名冲突和对齐问题。std::stringstd::stringemployee_adjustedstd::string

在实践中,该项目演示了如何计算和应用填充以实现大小要求,但它不认可或实现不安全的内存访问模式。
访问数组中每个成员的正确方法仍然是通过对象本身,而不是将内存视为 的数组。
SizeAdjusternameemployee_adjustedemployeestd::string

输出说明已实现尺寸调整,但未说明安全或符合标准的跨步访问,这超出了单独调整尺寸的能力。

-1赞 Ryan 11/10/2023 #3

如果您打算出于对齐目的计算填充,则只需将其应用于需要严格对齐的类型,这些类型通常是 POD(普通旧数据)类型。std::string 是一种具有非平凡构造函数和析构函数的类类型,因此通常不会以这种方式计算填充。

对于标准布局类型,通常只需要在成员之间填充以满足对齐要求。如果您使用的是将 std::string 作为成员的 employee 类型,编译器会为您处理对齐方式。如果出于某种原因(这种情况不常见)需要确保 employee 对象在内存边界上对齐,该边界是 std::string 大小的倍数,则可以手动对齐整个 employee 对象,而不是在其中插入填充。

若要确保员工结构与 std::string 的大小对齐,可以使用 alignas 说明符(C++11 及更高版本)来指定整个结构的对齐方式。通常使用最大成员的对齐要求,或与缓存行大小(如 64 字节)对齐,以避免在多线程环境中出现错误共享。

下面是如何使用 alignas 定义类的示例:

#include <string>
#include <iostream>
#include <cstddef>

// Class definition with specified alignment
struct alignas(alignof(std::max_align_t)) employee {
    std::string name; // 'std::string' is typically aligned to the max_align_t
    short salary;
    std::size_t age;
    // Padding is not manually needed because 'alignas' takes care of alignment
};

int main() {
    // Check the size of the class
    std::cout << "Size of std::string: " << sizeof(std::string) << std::endl;
    std::cout << "Size of employee: " << sizeof(employee) << std::endl;
    std::cout << "Alignment of employee: " << alignof(employee) << std::endl;

    // Instantiate an employee to show the address
    employee emp;
    std::cout << "Address of emp: " << &emp << std::endl;

    return 0;
}

在此代码中,employee 结构与 std::max_align_t 对齐,std::是对齐类型,保证在任何给定实现上都具有最严格(最大)的对齐要求。这样,您可以确保员工对象适当对齐,而无需手动计算填充。alignof 运算符返回类型参数的对齐要求。alignas 和 alignof 的使用使代码符合现代 C++ 实践并避免未定义的行为。

使员工成为模板的成员。

template <template<std::size_t> class Tmpl, std::size_t NeedMultipleOf>
struct adjust_padding {
    // Finding the padding size without recursion
    template <std::size_t N>
    static constexpr std::size_t padding_size() {
        for (std::size_t i = N;; ++i) {
            if (sizeof(Tmpl<i>) % NeedMultipleOf == 0) {
                return i;
            }
        }
    }

    using type = Tmpl<padding_size<0>()>;
};

// Struct to demonstrate padding adjustment
template <std::size_t K>
struct need_strided {
    double x;
    const char pad[K];
};

// Specialization for zero padding
template <>
struct need_strided<0> {
    double x;
};

// Using adjust_padding to ensure size is a multiple of 47
using strided = adjust_padding<need_strided, 47>::type;

// Generalized employee struct wrapped in a template
template <std::size_t K>
struct employee_wrapper {
    // Your employee struct goes here
    // Example: employee e;
};

在这个版本的 @n. m. 可以作为 AI 的例子中,adjust_padding 结构利用了编译时循环,

评论

0赞 alfC 11/11/2023
不好意思。但这也行不通。这个确切的代码是我在某个时候拥有的,但它通常不起作用。它为什么起作用只是偶然的。它最终在 clang Apple ARM 中失败了。归根结底,它(仅)与对齐有关,而与控制大小有关。没关系,在大多数平台中,对齐必须是 2 的幂。
0赞 alfC 11/11/2023
看这里,静态断言失败了,godbolt.org/z/bqoGvvvvd,实际上可能没有 alignas 的值,手动确定或计算可以解决问题。
0赞 Sedenion 11/12/2023 #4

无需制作模板或列出其成员两次。TL;DR:你可以深入到简洁的代码,比如:employee

#pragma pack(1) // Prevent padding at the end.
struct employee_base {
    std::complex<double> a;
};

using employee = PaddingHelper<employee_base>;

解释如下。


核心思想是(从 C++17 开始)您可以简单地从包含实际有效负载的类型派生,并保留聚合初始化语法(在 godbolt 上):

#pragma pack(1) // Prevent padding at the end of 'dummy'.
struct dummy {
    std::complex<double> a;
    double b;
    std::int64_t b2;
    bool c;
};

struct employee : dummy {
    char padding[
        (sizeof(dummy)/sizeof(decltype(dummy::a))+1)*sizeof(dummy::a) 
        - sizeof(dummy)]
        = {0};
};

static_assert(sizeof(employee) % sizeof(decltype(dummy::a)) == 0);

int main(){
    employee e{{41, 42, 43, false}};
}

这当然是非标准的,但 gcc、clang 和 MSVC 支持它。我们需要它来确保我们自己可以控制填充。#pragma pack

请注意,如果基类只有一个成员(或者其大小是第一个成员的倍数),则会增加额外的填充。为了解决这个问题,我们可以利用空基类优化(live on godbolt):

#pragma pack(1)
template <std::size_t size>
struct padding{
    char pad[size] = {};
};

template <>
struct padding<0>{};

template <class BaseClass, class FirstMember>
constexpr auto GetPaddingSize()
{
    constexpr auto Size = 
        (sizeof(BaseClass) % sizeof(FirstMember) == 0
            ? 0 
            : (sizeof(BaseClass)/sizeof(FirstMember)+1)*sizeof(FirstMember) 
                - sizeof(BaseClass));
    return Size;
}

#pragma pack(1) // Prevent padding at the end of 'dummy'.
struct dummy {
    std::complex<double> a;
};

struct employee : dummy, padding<GetPaddingSize<dummy, decltype(dummy::a)>()> {
};

static_assert(sizeof(employee) % sizeof(decltype(dummy::a)) == 0);
static_assert(sizeof(employee) == sizeof(decltype(dummy::a)));

int main(){
    [[maybe_unused]] employee e{{41}};
}

由于您正在处理聚合类型,因此可以使用 boost::p fr 通过 (godbolt) 摆脱第一个成员的显式规范:boost::pfr::tuple_element_t<0, BaseClass>

#include <boost/pfr.hpp>

#pragma pack(1)
template <std::size_t size>
struct padding{
    char pad[size] = {};
};

template <>
struct padding<0>{};

template <class BaseClass>
constexpr auto GetPaddingSize()
{
    using FirstMember = boost::pfr::tuple_element_t<0, BaseClass>;
    constexpr auto Size = 
        (sizeof(BaseClass) % sizeof(FirstMember) == 0
            ? 0 
            : (sizeof(BaseClass)/sizeof(FirstMember)+1)*sizeof(FirstMember) 
                - sizeof(BaseClass));
    return Size;
}

//--------------------

#pragma pack(1) // Prevent padding at the end.
struct employee_base {
    std::complex<double> a;
};

struct employee : employee_base, padding<GetPaddingSize<employee_base>()> {
};

static_assert(sizeof(employee) % sizeof(decltype(employee::a)) == 0);
static_assert(sizeof(employee) == sizeof(decltype(employee::a)));

//---------------------

#pragma pack(1) // Prevent padding at the end.
struct foo_base {
    std::size_t i;
    bool b;
};

struct foo : foo_base, padding<GetPaddingSize<foo_base>()> {
};

static_assert(sizeof(foo) % sizeof(decltype(foo::i)) == 0);

//---------------------

int main(){
    [[maybe_unused]] employee e{{41}};
    [[maybe_unused]] foo f{{41, false}};
}

然后你可以介绍

template <class BaseClass>
struct PaddingHelper : BaseClass, padding<GetPaddingSize<BaseClass>()> {};

允许写入,例如

#pragma pack(1) // Prevent padding at the end.
struct employee_base {
    std::complex<double> a;
};

using employee = PaddingHelper<employee_base>;

godbolt。当然,你可以做类似的事情,而不需要,你只需要添加第二个模板参数来接收第一个成员类型。 我认为没有比这更简洁的了。boost::pfrPaddingHelper

评论

0赞 Sedenion 11/12/2023
另一个想法是使用灵活的数组作为最后一个成员,并重载。换句话说,要有效地分配比类大小更多的内存。即在运行时而不是在编译时实现填充要求。例如,请参阅帖子(godbolt)。但是,请注意,当然不会知道额外的内存。它仅适用于堆上分配的对象。所以我不推荐它。另请参阅 gcc.gnu.org/onlinedocs/gcc/Zero-Length.htmloperator newsizeof
0赞 alfC 11/13/2023
有趣,很多东西要解开:)这里。第一个问题是,我现在需要双大括号吗,对吧?(这是我放弃继承选项的主要原因,但如果这是它所需要的,好吧)
1赞 Sedenion 11/13/2023
据我了解,我展示的第一个版本实际上并不需要双大括号。例如,请参阅这篇文章。但是,clang 会打印警告,因此我使用了双大括号。我倾向于认为这是叮当声中的错误。对于最终版本,gcc 也会打印一个警告,尽管是不同的警告。即使有双牙套。我认为这只是一个“请注意,您可能有错误”警告,可以忽略。
1赞 stackoverblown 11/13/2023 #5

试试这个

   struct employee {
       std::string name;
       union {  
          struct {
              short salary;
              std::size_t age;
          };
          std::string dummy;
       };
   };

在任何平台或编译器上,这应该是 2 * sizeof(std::string)。但以防万一,有人应该告诉我为什么不是这样,以及这会在哪个平台上失败!谢谢!

这可以很容易地推广到任何大小的数据字段的集合。只要有足够的 std::string 副本来覆盖原始结构中的总数大小,就可以在更改后的结构中强制执行 N * sizeof(std::string) 的大小,其中 N 是整数。

评论

0赞 Sedenion 11/14/2023
有趣的想法。但是,有两点:首先,你不能有一个匿名联合,因为你需要定义析构函数;否则,您将收到编译器错误。其次,仅当匿名结构中的数据大小小于或等于 时,这才有效。如果它更大,则必须添加其他元素(在它们自己的结构中)。godbolt 上的示例。std::stringdummy
0赞 stackoverblown 11/15/2023
@Sedenion 在 Visual Studio 中尝试过,那里没有编译器错误。我知道收集的数据大小必须小于或等于 std::string 才能正常工作。但这是OP给出的例子。您可以随时添加 std::string dummy1、dummy2、dummy3 的更多倍数来扩展大小。最重要的是,整个结构可以是sizeof(std::string)的倍数。