c++20 [[no_unique_address]] 中的新特性是什么?
what is the new feature in c++20 [[no_unique_address]]?
我已经阅读了新的 c++20 特性 no_unique_address
好几遍,我希望有人能用比下面这个取自 c++ 参考的例子更好的例子来解释和说明。
Explanation Applies to the name being declared in the declaration of a
non-static data member that's not a bit field.
Indicates that this data member need not have an address distinct from
all other non-static data members of its class. This means that if the
member has an empty type (e.g. stateless Allocator), the compiler may
optimise it to occupy no space, just like if it were an empty base. If
the member is not empty, any tail padding in it may be also reused to
store other data members.
#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';
}
- 有人可以向我解释一下此功能背后的目的是什么以及我应该在什么时候使用它吗?
- e1 和 e2 不能有相同的地址,但其中一个可以与 c[0] 共享,另一个与 c[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...
该功能背后的目的与您的引文中所述完全相同:“编译器可能会将其优化为不占用 space”。这需要两件事:
空对象。
一个对象需要一个可能为空的类型的非静态数据成员。
第一个很简单,你用的引语甚至拼出了一个重要的应用程序。 std::allocator
类型的对象 实际上 不存储任何内容。它只是一个基于 class 的接口,进入全局 ::new
和 ::delete
内存分配器。不存储任何类型数据(通常使用全局资源)的分配器通常称为“无状态分配器”。
分配器感知容器需要存储用户提供的分配器值(默认为该类型的默认构造分配器)。这意味着容器必须有一个该类型的子对象,它由用户提供的分配器值初始化。那个子对象在理论上占据了 space...
考虑 std::vector
。这种类型的常见实现是使用 3 个指针:一个指向数组的开头,一个指向数组有用部分的结尾,一个指向数组分配块的结尾。在 64 位编译中,这 3 个指针需要 24 个字节的存储。
无状态分配器实际上没有任何数据要存储。但在 C++ 中,每个对象的大小至少为 1。因此,如果 vector
将分配器存储为成员,每个 vector<T, Alloc>
将不得不占用至少 32 个字节, 甚至如果 分配器不存储任何内容。
常见的解决方法是从 Alloc
本身 派生 vector<T, Alloc>
。原因是基 class 子对象 不需要 大小为 1。如果基 class 没有成员并且没有非空基 classes,则允许编译器优化派生 class 中基数 class 的大小,使其实际上不占用 space。这称为“空基优化”(标准布局类型需要它)。
因此,如果您提供无状态分配器,从该分配器类型继承的 vector<T, Alloc>
实现的大小仍然只有 24 个字节。
但是有一个问题:你必须继承分配器。这真的很烦人。而且很危险。首先,分配器可以是 final
,这实际上是标准所允许的。其次,分配器的成员可能会干扰 vector
的成员。第三,它是人们必须学习的习语,这使它成为 C++ 程序员的民间智慧,而不是供任何人使用的明显工具。
因此,虽然继承是一种解决方案,但它并不是一个很好的解决方案。
这就是 [[no_unique_address]]
的用途。它将允许容器将分配器存储为成员子对象而不是基础 class。如果分配器为空,那么 [[no_unique_address]]
将允许编译器使其在 class 的定义中不占用 space。所以这样的 vector
仍然可以是 24 字节大小。
e1 and e2 cannot have the same address, but one of them can share with c[0] and the other with c1 can some one explain? why do we have such kind of relation ?
C++ 有一个基本规则,它的对象布局必须遵循。我称之为“unique identity rule”。
对于任意两个对象,至少满足以下条件之一:
它们必须有不同的类型。
它们在内存中必须有不同的地址。
它们实际上必须是同一个对象。
e1
和 e2
不是同一个对象,因此违反了#3。它们也共享相同的类型,因此违反了#1。因此,他们必须遵循#2:他们不能有相同的地址。在这种情况下,由于它们是同一类型的子对象,这意味着该类型的编译器定义的对象布局不能为它们在对象内提供相同的偏移量。
e1
和 c[0]
是不同的对象,所以#3 再次失败。但它们满足#1,因为它们有不同的类型。因此(根据 [[no_unique_address]]
的规则)编译器可以将它们分配给对象内的相同偏移量。 e2
和 c[1]
.
也是如此
如果编译器想要将class的两个不同成员分配给包含对象中的相同偏移量,那么它们必须是不同的类型(注意这是递归 通过它们的所有子对象)。因此,如果它们具有相同的类型,则它们必须具有不同的地址。
虽然其他答案已经很好地解释了,但让我从稍微不同的角度解释一下:
问题的根源是 C++ 不允许零大小的对象(即我们总是 sizeof(obj) > 0
)。
这本质上是 C++ 标准中非常基本的定义的结果:唯一身份规则(如 Nicol Bolas 所解释的),但也来自将“对象”定义为非空字节序列。
然而,这会在编写通用代码时导致不愉快的问题。这在某种程度上是意料之中的,因为这里的极端情况(-> 空类型)得到了特殊处理,偏离了其他情况的系统行为(-> 大小以非系统方式增加)。
效果是:
- Space 在使用无状态对象(即没有成员的 classes/structs)时被浪费了
- 禁止零长度数组。
由于在编写通用代码时很快就会遇到这些问题,因此已经进行了多次缓解尝试
- 空基class优化。这解决了 1) 部分案例
- 引入 std::array 允许 N==0。这解决了 2) 但仍有问题 1)
- [no_unique_address] 的引入,最终解决了 1) 的所有剩余情况。至少当用户明确要求时。
也许允许大小为零的对象是更清洁的解决方案,可以防止碎片化。但是,当您在 SO 上搜索大小为零的对象时,您会发现答案不同的问题(有时不具有说服力),并很快注意到这是一个有争议的话题。
允许大小为零的对象需要改变 C++ 语言的核心,并且考虑到 C++ 语言已经非常复杂的事实,标准委员会可能决定采用微创路线并引入了一个新属性。
与上面的其他缓解措施一起,它最终解决了由于不允许零大小对象而导致的所有问题。尽管从基本面来看它可能不是最好的解决方案,但它是有效的。
我已经阅读了新的 c++20 特性 no_unique_address
好几遍,我希望有人能用比下面这个取自 c++ 参考的例子更好的例子来解释和说明。
Explanation Applies to the name being declared in the declaration of a non-static data member that's not a bit field.
Indicates that this data member need not have an address distinct from all other non-static data members of its class. This means that if the member has an empty type (e.g. stateless Allocator), the compiler may optimise it to occupy no space, just like if it were an empty base. If the member is not empty, any tail padding in it may be also reused to store other data members.
#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';
}
- 有人可以向我解释一下此功能背后的目的是什么以及我应该在什么时候使用它吗?
- e1 和 e2 不能有相同的地址,但其中一个可以与 c[0] 共享,另一个与 c[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...
该功能背后的目的与您的引文中所述完全相同:“编译器可能会将其优化为不占用 space”。这需要两件事:
空对象。
一个对象需要一个可能为空的类型的非静态数据成员。
第一个很简单,你用的引语甚至拼出了一个重要的应用程序。 std::allocator
类型的对象 实际上 不存储任何内容。它只是一个基于 class 的接口,进入全局 ::new
和 ::delete
内存分配器。不存储任何类型数据(通常使用全局资源)的分配器通常称为“无状态分配器”。
分配器感知容器需要存储用户提供的分配器值(默认为该类型的默认构造分配器)。这意味着容器必须有一个该类型的子对象,它由用户提供的分配器值初始化。那个子对象在理论上占据了 space...
考虑 std::vector
。这种类型的常见实现是使用 3 个指针:一个指向数组的开头,一个指向数组有用部分的结尾,一个指向数组分配块的结尾。在 64 位编译中,这 3 个指针需要 24 个字节的存储。
无状态分配器实际上没有任何数据要存储。但在 C++ 中,每个对象的大小至少为 1。因此,如果 vector
将分配器存储为成员,每个 vector<T, Alloc>
将不得不占用至少 32 个字节, 甚至如果 分配器不存储任何内容。
常见的解决方法是从 Alloc
本身 派生 vector<T, Alloc>
。原因是基 class 子对象 不需要 大小为 1。如果基 class 没有成员并且没有非空基 classes,则允许编译器优化派生 class 中基数 class 的大小,使其实际上不占用 space。这称为“空基优化”(标准布局类型需要它)。
因此,如果您提供无状态分配器,从该分配器类型继承的 vector<T, Alloc>
实现的大小仍然只有 24 个字节。
但是有一个问题:你必须继承分配器。这真的很烦人。而且很危险。首先,分配器可以是 final
,这实际上是标准所允许的。其次,分配器的成员可能会干扰 vector
的成员。第三,它是人们必须学习的习语,这使它成为 C++ 程序员的民间智慧,而不是供任何人使用的明显工具。
因此,虽然继承是一种解决方案,但它并不是一个很好的解决方案。
这就是 [[no_unique_address]]
的用途。它将允许容器将分配器存储为成员子对象而不是基础 class。如果分配器为空,那么 [[no_unique_address]]
将允许编译器使其在 class 的定义中不占用 space。所以这样的 vector
仍然可以是 24 字节大小。
e1 and e2 cannot have the same address, but one of them can share with c[0] and the other with c1 can some one explain? why do we have such kind of relation ?
C++ 有一个基本规则,它的对象布局必须遵循。我称之为“unique identity rule”。
对于任意两个对象,至少满足以下条件之一:
它们必须有不同的类型。
它们在内存中必须有不同的地址。
它们实际上必须是同一个对象。
e1
和 e2
不是同一个对象,因此违反了#3。它们也共享相同的类型,因此违反了#1。因此,他们必须遵循#2:他们不能有相同的地址。在这种情况下,由于它们是同一类型的子对象,这意味着该类型的编译器定义的对象布局不能为它们在对象内提供相同的偏移量。
e1
和 c[0]
是不同的对象,所以#3 再次失败。但它们满足#1,因为它们有不同的类型。因此(根据 [[no_unique_address]]
的规则)编译器可以将它们分配给对象内的相同偏移量。 e2
和 c[1]
.
如果编译器想要将class的两个不同成员分配给包含对象中的相同偏移量,那么它们必须是不同的类型(注意这是递归 通过它们的所有子对象)。因此,如果它们具有相同的类型,则它们必须具有不同的地址。
虽然其他答案已经很好地解释了,但让我从稍微不同的角度解释一下:
问题的根源是 C++ 不允许零大小的对象(即我们总是 sizeof(obj) > 0
)。
这本质上是 C++ 标准中非常基本的定义的结果:唯一身份规则(如 Nicol Bolas 所解释的),但也来自将“对象”定义为非空字节序列。
然而,这会在编写通用代码时导致不愉快的问题。这在某种程度上是意料之中的,因为这里的极端情况(-> 空类型)得到了特殊处理,偏离了其他情况的系统行为(-> 大小以非系统方式增加)。
效果是:
- Space 在使用无状态对象(即没有成员的 classes/structs)时被浪费了
- 禁止零长度数组。
由于在编写通用代码时很快就会遇到这些问题,因此已经进行了多次缓解尝试
- 空基class优化。这解决了 1) 部分案例
- 引入 std::array 允许 N==0。这解决了 2) 但仍有问题 1)
- [no_unique_address] 的引入,最终解决了 1) 的所有剩余情况。至少当用户明确要求时。
也许允许大小为零的对象是更清洁的解决方案,可以防止碎片化。但是,当您在 SO 上搜索大小为零的对象时,您会发现答案不同的问题(有时不具有说服力),并很快注意到这是一个有争议的话题。 允许大小为零的对象需要改变 C++ 语言的核心,并且考虑到 C++ 语言已经非常复杂的事实,标准委员会可能决定采用微创路线并引入了一个新属性。
与上面的其他缓解措施一起,它最终解决了由于不允许零大小对象而导致的所有问题。尽管从基本面来看它可能不是最好的解决方案,但它是有效的。