C++20[[NO_UNIQUE_ADDRESS]]中的新功能是什么?

2022-05-16 00:00:00 attributes c++ c++20

我已经读了好几遍C++20的新特性no_unique_address,我希望有谁能从C++参考中用一个比下面这个例子更好的例子来解释和说明。

解释适用于在 不是位字段的非静态数据成员。

表示此数据成员的地址不需要不同于 其类的所有其他非静态数据成员。这意味着如果 成员具有空类型(例如无状态分配器),则编译器可以 将其优化为不占用任何空间,就像它是一个空基地一样。如果 该成员不是空的,其中的任何尾部填充也可以重复使用 存储其他数据成员。

#include <iostream>
 
struct Empty {}; // empty class
 
struct X {
    int i;
    Empty e;
};
 
struct Y {
    int i;
    [[no_unique_address]] Empty e;
};
 
struct Z {
    char c;
    [[no_unique_address]] Empty e1, e2;
};
 
struct W {
    char c[2];
    [[no_unique_address]] Empty e1, e2;
};
 
int main()
{
    // e1 and e2 cannot share the same address because they have the
    // same type, even though they are marked with [[no_unique_address]]. 
    // However, either may share address with c.
    static_assert(sizeof(Z) >= 2);
 
    // e1 and e2 cannot have the same address, but one of them can share with
    // c[0] and the other with c[1]
    std::cout << "sizeof(W) == 2 is " << (sizeof(W) == 2) << '
';
}
  1. 有人能给我解释一下这个功能背后的目的是什么吗?我应该在什么时候使用它?
  2. e1和e2不能有相同的地址,但其中一个可以与c[0]共享,另一个可以与c[1]共享谁能解释一下?为什么我们会有这样的关系?

解决方案

该功能背后的目的与您的报价中所述完全相同:编译器可能会对其进行优化以不占用任何空间。这需要两件事:

  1. 一个空对象。

  2. 希望具有可能为空的类型的非静态数据成员的对象。

第一个非常简单,您使用的引语甚至解释了一个重要的应用程序。std::allocator类型的对象实际上不存储任何内容。它只是全局::new::delete内存分配器的一个基于类的接口。不存储任何类型的数据(通常通过使用全局资源)的分配器通常称为无状态分配器。

需要支持分配器的容器来存储用户提供的分配器的值(默认为该类型的默认构造的分配器)。这意味着容器必须具有该类型的子对象,该子对象由用户提供的分配器值进行初始化。而那个子物体占据了空间。理论上。

考虑std::vector。这种类型的常见实现是使用3个指针:一个用于数组的开始,一个用于数组的有用部分的结束,以及一个用于数组的已分配块的结束。在64位编译中,这3个指针需要24字节的存储空间。

无状态分配器实际上没有任何要存储的数据。但在C++中,每个对象的大小至少为1。因此,如果vector将分配程序存储为成员,则每个vector<T, Alloc>将至少占用32个字节,即使分配程序不存储任何内容。

解决此问题的常见方法是从Alloc本身派生vector<T, Alloc>。原因是基类子对象不要求大小为1。如果基类没有成员,也没有非空基类,则允许编译器优化派生类中基类的大小,使其不会实际占用空间。这称为空基优化(对于标准布局类型是必需的)。

因此,如果提供无状态分配器,则从该分配器类型继承的vector<T, Alloc>实现的大小仍然只有24个字节。

但有一个问题:您必须从分配器继承。这真的令人讨厌。而且很危险。首先,分配器可以是final,这实际上是标准允许的。第二,分配器的成员可能会干扰vector的成员。第三,这是一个人们必须学习的习惯用法,这使它成为C++程序员的民间智慧,而不是他们中任何人都可以使用的明显工具。

因此,虽然继承是一种解决方案,但它不是一个很好的解决方案。

这就是[[no_unique_address]]的用途。它将允许容器将分配器存储为成员子对象,而不是基类。如果分配器为空,则[[no_unique_address]]将允许编译器使其不占用类定义中的空间。因此,这样的vector仍可能是24字节大小。


e1和e2不能有相同的地址,但其中一个可以与c[0]共享,另一个可以与c1共享,谁能解释一下?为什么我们会有这样的关系?

C++有一个基本规则,即其对象布局必须遵循。我称之为unique identity rule&q;。

对于任意两个对象,必须至少满足以下条件之一:

  1. 它们必须具有不同的类型。

  2. 它们在内存中必须具有不同的地址。

  3. 它们实际上必须是同一对象。

e1e2不是同一个对象,因此违反了#3。它们也共享相同的类型,因此#1被违反。因此,他们必须遵循第二点:他们不能有相同的地址。在这种情况下,由于它们是相同类型的子对象,这意味着此类型的编译器定义的对象布局不能在对象中为它们提供相同的偏移量。

e1c[0]是不同的对象,因此#3再次失败。但它们满足第一点,因为它们有不同的类型。因此(受制于[[no_unique_address]]的规则),编译器可以将它们分配给对象中的相同偏移量。e2c[1]也是如此。

如果编译器希望将类的两个不同成员分配给包含对象内的相同偏移量,则它们必须是不同类型的(请注意,这是通过它们的所有子对象递归的)。因此,如果它们具有相同的类型,则它们必须具有不同的地址。

相关文章