C++11对于内存对齐的支持

        对齐的数据有助于提高内存的访问效率以及减少程序运行期间因为内存未对齐导致硬件抛出错误的可能。因此在c++中,数据的对齐是必不可少的,对于系统而言在默认情况下也是坚持数据对齐这一准则的。关于内存对齐的详细内容可见《C++ 内存对齐》

        在旧式的c++语言中,我们可以通过sizeof关键字查看数据的长度,但却无法对数据的内存对齐的方式进行查询,以及执行相关的对齐操作。这使得当程序涉及有关数据对齐的一些特性时将会变得异常困难。

        而在c++11新标准中,为了支持内存对齐的操作,引入了alignofalignas等关键字。同时也引入了基于内存对齐的存储空间管理工具:

  • std::aligned_storage
  • std::aligned_union

        这两个工具均定义在头文件<type_traits>中,接下来就来详细谈谈这两个工具。


std::aligned_storage

  std::aligned_storage 是 C++11 引入的一个模板结构,用于创建具有特定大小和对齐要求的未初始化存储空间。它主要用于需要手动管理内存对齐的场景,确保在使用某些类型时不会出现对齐问题。 其语法如下:  

template <std::size_t Len, std::size_t Align = alignof(std::max_align_t)>
struct aligned_storage;
  • Len:所要分配的存储空间的大小(以字节为单位)。
  • Align:存储空间的对齐要求(以字节为单位)。其默认值为std::max_align_t

什么是std::max_align_t

  std::max_align_t是对齐值可能的最大值,也就是该值将会满足所有数据类型的对齐要求。该值是由编译器和系统共同决定的。  

        例如,在 x86-64 平台上,最大对齐值通常是 16 个字节,因为 long double 类型的对齐值是 16 个字节。在 ARM 平台上,最大对齐值可能会是 8 个字节,因为 double 类型的对齐值是 8 个字节。

        例如,在我的平台上,最大对齐值为8字节:

  #include <cstddef>  //max_align_t定义在此头文件内
  std::cout << "Alignas:" << alignof(std::max_align_t) << std::endl;
  --Output:
  8

        使用如下语句可以得到一个类型:

 std::aligned_storage<20,4>::type  //注意,这是一个类型

上述语句定义了一个20字节为大小,4字节对齐(地址为4的倍数)的内存块类型,使用该类型可以在堆空间或栈空间上分配该内存块。

  {
    std::aligned_storage<20,4>::type MyBlock;  //栈上开辟内存块
    std::cout << "The Address:" << &MyBlock << std::endl;
    std::cout << "The Size:" << sizeof(MyBlock) << std::endl;
  }
  --Output:
  The Address:3D2A4FF7C4
  The Size:20

        上述用例在栈上开辟了一块大小为20字节,以4字节对齐的内存块,在当前作用域有效,离开作用域后资源被自动回收

  {
    typedef std::aligned_storage<20,4>::type blockCustom;
    auto *ptr_to_block = new blockCustom;  //堆上开辟内存块
    std::cout << "The Address:" << ptr_to_block << std::endl;
    std::cout << "The Size:" << sizeof(*ptr_to_block) << std::endl;
    delete ptr_to_block;
  }
  --Output:
  The Address:17F659AF9E0
  The Size:20

        上述用例在堆上开辟了一块大小为20字节,以4字节对齐的内存块,应注意其生命周期的管理。

        在使用storageunion工具时,应使用typedefusing(C11)为其冗长的类型取一个清晰易读的别名,方便后续的使用以及使代码的可读性更好:

  {
    typedef std::aligned_storage<sizeof(Entity),alignof(Entity)>::type blockCustom;
    using _blockCustom = std::aligned_storage<sizeof(Entity),alignof(Entity)>::type;
    blockCustom storage_1;  //别名创建方便快捷
  }

        开辟出来的空间是未被初始化的,想要在其中构造对象应该使用 placement new

class Entity {
 public:
  Entity() { std::cout << "Constructor called" << std::endl; }
  ~Entity() { std::cout << "Destructor called" << std::endl; }
 private:
  int a;
  double b;
  char c;
};
 void test() {
  {
    using _blockCustom = std::aligned_storage<40>::type;
    _blockCustom storage;  //栈上开辟内存块
    auto *ptr_to_ent = new (&storage) Entity();  //Placement New
  }
}
--Output:
Constructor called

        可以看到,我们使用Placement New成功在该内存块中构造了一个Entity对象。有朋友可能已经发现,我们析构函数并没有被调用,虽然我们的内存块是开辟在栈空间中的,可以在作用域离开时自动回收内存资源,但是这么做并不好。

        假如Entity的析构函数中需要释放一些其余动态资源,析构函数不被调用则很可能会导致未定义行为,因此我们必须保证析构函数被手动调用

 ptr_to_ent->~Entity();

亦或是你也可以使用智能指针进行管理

  {
    using _blockCustom = std::aligned_storage<40>::type;
    auto ptr_to_block = new _blockCustom;  //堆上开辟内存块
    std::unique_ptr<Entity> smart_ptr_ent(new (ptr_to_block) Entity());
  }
  --Output:
  Constructor called
  Destructor called

注意!

        使用智能指针管理的方法只能用在堆空间上,不能用在栈空间上!因为智能指针会尝试调用 delete,而这对于栈上分配的内存是不适用的,会导致未定义行为。例如下列代码,是无法正常运行的。如果必须要用则应给定自定义的删除器!(详见《C++11 智能指针》

  {
    using _blockCustom = std::aligned_storage<40>::type;
    _blockCustom storage;  //栈上开辟内存块
    std::unique_ptr<Entity> smart_ptr_ent(new (&storage) Entity());  //不能用智能指针!
  }

        当然,你也可以使用c++中封装好的allocator(空间配置器)来在所分配的内存块上创建对象。如下所示:

 {
    using _blockCustom = std::aligned_storage<40>::type;
    _blockCustom storage;  //栈上开辟内存块
    std::allocator<Entity> alloc;
    alloc.construct((Entity *)&storage);
    alloc.destroy((Entity *)&storage); 
  }
--Output:
Constructor called
Destructor called

总而言之:

  • std::aligned_storage是一个在C++11标准中引入的,支持内存对齐的内存分配工具。
  • 它提供了一种灵活的方式来创建未初始化的存储空间,并允许在其中构造对象。
  • 所分配的存储空间是未初始化的,因此在使用之前需要显式构造对象。
  • 其只提供存储空间,不会自动管理对象的生命周期,因此需要手动调用构造和析构函数 (注意:若内存分配在栈空间上则不建议使用智能指针管理该空间中的对象,若仍要使用请务必自定义删除器)。
  • 确保提供的大小和对齐要求是正确的,以避免未定义行为。

    该工具的其中一个应用场景是配合其它工具,实现类型擦除技术。所谓类型擦除技术,是一种编程技术,用于在编译时去掉类型信息,从而实现不同类型的对象在内存中共享相同的布局和大小

struct Any {
  static constexpr size_t alloc_size = 64;
  static constexpr size_t alignment = alignof(std::max_align_t);

  typedef std::aligned_storage<alloc_size,alignment>::type block_type;
  block_type storage;

  template<typename T>
  void set(T &&value) {
    new (&storage) T(std::forward<T>(value));
  }

  template<typename T>
  T* get() {
    return reinterpret_cast<T *>(&storage);
  }
};

void myfunc() {
  Any any;
  any.set(5.44);
  std::cout << *any.get<double>() << std::endl;

  any.set(std::string("Hello,Cplusplus"));
  std::cout << *any.get<std::string>() << std::endl;
}

        正如上述用例所示:Any 类提供了一种在固定大小的内存块中存储任意类型对象的机制。我们通过aligned_storage工具配合placement new以及完美转发技术实现了一个最为简单的类型擦除应用的示例。  


std::aligned_storage_t

  std::aligned_storage_t 是 C++11 中引入的 std::aligned_storage 的简化版本,专门用于生成一个具有指定大小和对齐要求的类型别名。与 std::aligned_storage 不同的是,它省去了显式访问::type 的麻烦,从而提高了代码的可读性和简洁性(其实就是使用using封装了一下) 。

        从C++14之后,std::aligned_storage_t就被定义为了一个类型别名。其语法原型如下:  

template <std::size_t Len, std::size_t Align = default>
using std::aligned_storage_t = typename std::aligned_storage<Len, Align>::type;

这个别名直接生成一个满足 Len 和 Align 要求的类型,简化了对 std::aligned_storage 的使用。

  {
    std::aligned_storage_t<16,alignof(Entity)> storage;  //栈上开辟内存块

    typedef std::aligned_storage_t<16,alignof(Entity)> blockType;  //再封装一层
    blockType storage;
  }

如上述用例,相比起原来确实更加直观,简洁。但是在使用时仍然建议再封装一层。


std::aligned_union

  std::aligned_union` 是从 C++11 引入的,用于计算满足对齐要求的内存块大小和对齐方式,该内存块可以用来存储任意一种指定的类型 。 这是一个类模板,会自动计算所需的对齐和存储大小,以便满足多个类型的存储需求 。其语法定义如下:

template<std::size_t Len, typename... Types>
struct std::aligned_union;
  • Len : 该联合体的最小存储大小。
  • Types : 是一组类型,这些类型是将被联合存储的可能选项。

 类成员:

  • type : 一个类型别名,表示满足所有对齐和大小需求的联合体 (也就是获取类型)。
  • alignment_value : 一个 static constexpr 值,表示满足所有类型对齐需求的对齐值
  • __strictest 是一个类型,它是所有给定类型的最严格对齐要求的交集。也就是说,__strictest 是一个类型,它的对齐方式和大小都满足给定类型的最严格要求。这里只做简单了解。
  • _s_len 是一个内部方法,用于获取 std::aligned_union 的大小。同样这里不深入展开。

        来看下面一个例子:

class A {
 public:
  A() { std::cout << "A Consturct." << std::endl; }
  ~A(){}
  int a;
  char b;
  double c;
};

class B {
 public:
  B() { std::cout << "B Consturct." << std::endl; }
  ~B(){}
  int* a;
  char b;
  double c;
};

  {
    typedef std::aligned_union<0,A,B>::type MyUnion;
    MyUnion storage;
    auto* ptr_to_A = new(&storage) A();
    ptr_to_A->~A();  //手动调用析构

    auto* ptr_to_B = new(&storage) B();
    ptr_to_B->~B();
  }

        上述用例中,我们通过aligned_union工具,设置了自己的一个共用体类型:上述代码中将最小存储大小设置为0,当所设置的最小存储大小小于最大对象的内存时,根据计算式max(sizeof(A),sizeof(B)),会取能容纳一个在所给出的类型中(上述为A,B类型中)占用内存空间最大对象的内存来作为该内存块的内存。并且根据计算式max(alignof(A), alignof(B))确定内存块的对齐方式。

        根据内存对齐的原则,我们可以知道:sizeof(A) = 16, sizeof(B) = 24alignof(A) = alignof(B) = 8(8字节对齐)。因此可以确定MyUnion大小为24字节,对齐方式为8字节对齐。

        所以上述在栈上所分配的内存块(storage)就可以用于存储一个A对象,或存储一个B对象。但是注意,以下这种写法是不可取的!

 {
    typedef std::aligned_union<0,A,B>::type MyUnion;
    MyUnion storage;
    char* flag = reinterpret_cast<char *>(&storage);
    auto* ptr_to_A = new(&storage) A();
    flag += sizeof(A);  //指针偏移
    auto* ptr_to_B = new(flag) B();
    ptr_to_A->~A();  //手动调用析构
    ptr_to_B->~B();
  }

   storage 是一个 MyUnion 对象,大小为 24 字节,实际只能容纳一个 A 或一个 B 对象。偏移 16 字节后构造B对象会导致A和B在同一个内存块中重叠

   A 使用了 storage 的前 16 字节。B 使用了从第 16 字节到第 24 字节的内存,这不仅会导致 B 的数据部分超出 storage 范围,产生内存溢出,同时,如果 flag 的地址不满足 B 的对齐需求,可能会触发未定义行为,而且,A 和 B 同时存在,析构时如果 A 和 B 的内存范围重叠,析构函数可能破坏彼此的数据。

        为了避免内存重叠的问题,std::aligned_union 分配的内存块通常只用于存储一个对象。该工具可用于在内存池中向用户分配内存块。

        关于std::aligned_union_t,其用法与std::aligned_storage_t一致,是一个简化了的版本,此处仅给出如下示例:

 {
    typedef std::aligned_union_t<0,A,B> MyUnion;
    MyUnion alloc_union;
    auto *ptr_to_A = new(&alloc_union) A();
    ptr_to_A->~A();
  }

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐