c++20[[no.unique_address]]中的新功能是什么

what is the new feature in c++20 [[no_unique_address]]?

本文关键字:是什么 新功能 address no unique c++20      更新时间:2023-10-16

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

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

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

#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) << 'n';
}
  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++有一条基本规则,它的对象布局必须遵循。我称之为";唯一身份规则";。

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

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

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

  3. 它们实际上一定是同一个对象。

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

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

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

为了理解[[no_unique_address]],让我们来看看unique_ptr。它有以下签名:

template<class T, class Deleter = std::default_delete<T>>
class unique_ptr;

在这个声明中,Deleter表示一个类型,它提供了用于删除指针的操作。

我们可以这样实现unique_ptr

template<class T, class Deleter>
class unique_ptr {
T* pointer = nullptr;
Deleter deleter;
public:
// Stuff
// ...
// Destructor:
~unique_ptr() {
// deleter must overload operator() so we can call it like a function
// deleter can also be a lambda
deleter(pointer);
}
};

那么这个实现有什么问题呢我们希望unique_ptr尽可能轻。理想情况下,它应该与常规指针大小完全相同。但是因为我们有Deleter成员,所以unqiue_ptr最终将至少为16个字节:指针为8个,然后是8个额外的字节来存储Deleter,即使Deleter为空

[[no_unique_address]]解决了这个问题:

template<class T, class Deleter>
class unique_ptr {
T* pointer = nullptr;
// Now, if Deleter is empty it won't take up any space in the class
[[no_unique_address]] Deleter deleter;
public:
// STuff...

虽然其他答案已经很好地解释了它,但让我从一个稍微不同的角度来解释它:

问题的根源是C++不允许零大小的对象(即我们总是有sizeof(obj) > 0)。

这本质上是C++标准中非常基本的定义的结果:唯一恒等式规则(正如Nicol Bolas所解释的);对象";作为非空字节序列。

然而,这会导致在编写泛型代码时出现令人不快的问题。这在一定程度上是意料之中的,因为在这里,角落病例(->空型)受到了特殊的处理,这偏离了其他病例的系统行为(->大小以非系统的方式增加)。

效果是:

  1. 当使用无状态对象(即没有成员的类/结构)时,会浪费空间
  2. 禁止使用零长度数组

由于在编写通用代码时很快就会遇到这些问题,因此已经尝试了几种缓解的方法

  • 空基类优化。这解决了1)对于一个子集的情况
  • 引入std::数组,允许N==0。这解决了2),但仍然存在问题1)
  • 引入[no_unique_address],最终解决了1)所有剩余情况。至少当用户明确要求时是这样
  • std::is_empty简介。需要,因为明显的sizeof不起作用(如sizeof(Empty) >= 1)。(感谢Dwayne Robinson)

也许允许零大小的对象是更清洁的解决方案,可以防止碎片*)。然而,当你在SO上搜索零大小的对象时,你会发现有不同答案的问题(有时不令人信服),并很快注意到这是一个有争议的话题。允许零大小的对象需要改变C++语言的核心,鉴于C++语言已经非常复杂,标准委员会可能决定采用最小侵入性的方法,并引入了一个新的属性。

结合上面的其他缓解措施,它最终解决了由于不允许零大小对象而产生的所有问题。尽管从根本角度来看,这可能不是最好的解决方案,但它是有效的。

*)对我来说,零大小类型的唯一标识规则无论如何都没有多大意义。为什么我们首先要让程序员选择的无状态对象(即没有非静态数据成员)具有唯一的地址?地址是对象的某种(不可变的)状态,如果程序员想要一种状态,他们可以添加一个非静态数据成员。