为什么使用“std::aligned_storage”据称会因无法“提供存储”而导致 UB?

发布于 2025-01-20 17:45:56 字数 948 浏览 1 评论 0原文

启发:为什么要在C ++ 23中弃用STD :: Aligned_storage,而是要使用什么?

灵感 >(这将reprecect std :: Aligned_storage)说:

使用对齐_*调用未定义的行为(类型无法提供存储。)

这是指 [Into.Object]/3

如果与“ n ”或类型“ n std :: byte>的数组”([cstddef.syn]),如果以下情况,则数组为创建对象提供存储的存储空间。

...继续使用一些定义中的“提供存储”一词,但是我看不到它在任何地方都说使用其他类型作为安置新的存储(无法“提供存储”)会导致UB。

因此,问题是:什么使std :: aligned_storage在使用时会导致UB?

Inspired by: Why is std::aligned_storage to be deprecated in C++23 and what to use instead?

The linked proposal P1413R3 (that deprecates std::aligned_storage) says that:

Using aligned_* invokes undefined behavior (The types cannot provide storage.)

This refers to [intro.object]/3:

If a complete object is created ([expr.new]) in storage associated with another object e of type “array of N unsigned char” or of type “array of N std​::​byte” ([cstddef.syn]), that array provides storage for the created object if: ...

The standard then goes on to use the term "provides storage" in a few definitions, but I don't see it saying anywhere that using a different type as storage for placement-new (that fails to "provide storage") causes UB.

So, the question is: What makes std::aligned_storage cause UB when used for placement-new?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

柏拉图鍀咏恒 2025-01-27 17:45:56

该论文似乎是错误的。

如果 std :: aligned_storage_t无法“提供存储”,则然后大多数用途会间接导致UB(见下文)。

但是,std :: aligned_storage_t实际上可以“提供存储”似乎未指定。使用alignas(y)unsigned char arr [x];成员(似乎是) dies “提供存储”的常见实现://eel.is/c++draft/basic.memobj#intro.object-3“ rel =“ noreferrer”> [Into.Object]/3 ,即使您也是如此将整个结构的地址传递到位置新的,而不是数组。即使目前尚未强制执行此特定实施,我认为强制这将是一个简单的非破坏变化。


如果std :: aligned_storage_t实际上没有“提供存储”,则大多数用例都会导致UB:

blopement new进入无法“提供存储”的对象本身是合法的,但是.. 。

​下次您访问任何一个时,您将获得UB。

即使aligned_storage_t不嵌套在其他对象中(这很少见),您必须在销毁它时要小心,因为呼叫其驱动器也会导致UB,因为它的寿命已经结束。

[basic.life] /1.5

... T型对象O的寿命结束时:

- 对象所占据的存储...由未嵌套在[对象]

中的对象重复使用

intro.object/4 < /a>

如果:

,一个对象A嵌套在另一个对象B中。

- a是B或

的子对象

- B提供A或

的存储空间

- 存在一个对象c,其中a嵌套在c中,c嵌套在b。


The paper appears to be wrong on this.

If std::aligned_storage_t failed to "provide storage", then most uses of it would indirectly cause UB (see below).

But whether std::aligned_storage_t can actually "provide storage" appears to be unspecified. A common implementation that uses a struct with alignas(Y) unsigned char arr[X]; member (seemingly) does "provide storage" according to [intro.object]/3, even if you pass the address of the whole structure into placement-new, rather than the array. Even though this specific implementation isn't mandated now, I believe mandating it would be a simple non-breaking change.


If std::aligned_storage_t actually didn't "provide storage", then most use cases would cause UB:

Placement-new into an object that fails to "provide storage" is legal by itself, but...

This ends the lifetime of the object that failed to "provide storage" (aligned_storage_t), and, recursively, all enclosing objects. The next time you access any of those, you get UB.

Even if aligned_storage_t is not nested within other objects (which is rare), you'd have to be careful when destroying it, since calling its destructor would also cause UB, since its lifetime has already ended.

[basic.life]/1.5

... The lifetime of an object o of type T ends when:

— the storage which the object occupies ... is reused by an object that is not nested within [the object]

intro.object/4

An object a is nested within another object b if:

—a is a subobject of b, or

— b provides storage for a, or

— there exists an object c where a is nested within c, and c is nested within b.

魂牵梦绕锁你心扉 2025-01-27 17:45:56

C ++标准使一组非常有限的类型集作为其他对象的存储。可以用作其他对象存储的类型集不能将对齐方式包装到其类型中。

想象一下:

template<std::size_t N>
using bytes=std::byte[N];
template<std::size_t S, std::size_t A>
struct alignas(A) aligned{
  bytes<S> data;
};

您不能使用&amp; Aligned&lt; 12,4&gt;安全地存储另一个对象。您不能制作与此属性保持一致的Typedef。

您可以使用Aligned&lt; 12,4&gt;一个; &amp; a.data或类似,但这在句法上有所不同。

现在,标准可以通过添加措辞来解决它。但是,对齐存储的现有定义没有这种魔术措辞,并且C ++中没有构造可以让aligned_storage_t的属性用户在没有此类措辞的情况下期望。我的意思是,UB是UB,因此编译器可以自由地解释您的程序,就好像它是使用该措辞的语言的程序一样……但这是用核弹来打动标准错误。

The C++ standard lets a very restricted set of types serve as storage for other objects. The set of types that can serve as storage for other objects cannot themselves have alignment packaged into their type.

Imagine:

template<std::size_t N>
using bytes=std::byte[N];
template<std::size_t S, std::size_t A>
struct alignas(A) aligned{
  bytes<S> data;
};

You cannot use &aligned<12,4> to store another object safely. You cannot make a typedef that carries alignment with it with this property.

You could use aligned<12,4> a; &a.data or similar, but that is syntactically different.

Now, the standard could get around it by adding wording; but the aligned storage existing definition does not have this magic wording, and no construct in C++ can have the properties users of aligned_storage_t are expecting without such wording. I mean, UB is UB, so the compiler is free to interpret your program as if it was a program in a language with that wording... but that is swatting a standard error with a nuclear bomb.

昇り龍 2025-01-27 17:45:56

前言:这是一个很长的答案。对此感到抱歉!基本的答案是简短而甜蜜的。但有很多不好的论点,所以有很多基础知识需要触及。

该提案声称“使用aligned_*会调用未定义的行为(类型无法提供存储。)”,但没有提供额外的讨论、证明或对标准中语言的引用。

这似乎是一种“语言律师”立场,基于对 C++ 标准 6.7.2 中使用的“提供存储”的过度字面解释。该声明似乎来自第 6.7.2.3 节,其中规定:

如果在与“N unsigned char 数组”类型或“N std::byte 数组”类型(17.2.1)的另一个对象 e 关联的存储中创建了一个完整的对象(7.6.2.7),则该数组为创建的对象提供存储...

然后,明显的论点继续说,换句话说,“好吧,aligned_storage_t不是一个字节数组。它是一个包含字节数组的结构/联合/类。所以虽然该数组可以提供存储,aligned_storage_t 本身不能。”这个论点的明显问题是......如果第 6.7.2.3 节在这里不适用,标准中的其他一些部分仍然可能适用。标准中还有很多其他“提供存储”的“东西”。

但所有这些都无关紧要。没有争议的是,aligned_storage_t 中包含的 unsigned char 数组可以提供存储。如果我们可以获得指向该存储的指针(例如,用于放置 new 或普通可复制类型的 memcpy),我们就可以访问该存储。

根据最清晰语言的标准,我们可以通过指向aligned_storage_t的指针获得一个unsigned char*(它指向数组存储的开头)。这是定义的行为,用于获取指向aligned_storage_t内包含的“存储”的指针:

std::aligned_storage_t<sizeof(T), alignof(T)> buf;
unsigned char* storage = reinterpret_cast<unsigned char*>(&buf);
T* tptr = new(storage) T;
tptr->~T();

为什么?因为aligned_storage_t 要么作为与unsigned char 数组作为非静态数据成员的联合实现,要么作为标准布局类(例如,POD 结构体)以数组作为第一个非静态数据成员实现。

请参阅第 72-73 页上的标准第 6.8.2 节复合类型:

4 两个对象 a 和 b 是指针可相互转换的,如果:

(4.2) — 一个是联合对象,另一个是该对象的非静态数据成员 (11.5),...

(4.3) — 一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,...

术语“指针可互转换”意味着:

如果两个对象是指针可相互转换的,那么它们具有相同的地址,并且可以获得
通过reinterpret_cast (7.6.1.9)从一个指针指向另一个指针。 [注:数组对象及其
第一个元素不可进行指针互换,即使它们具有相同的地址。 ——尾注]

就是这样。 aligned_storage_t 中包含的数组确实提供了存储,我们可以通过“指针可互转换”规则合法地获取指向该存储的指针。


编辑:为了解决评论中与语言律师的讨论,实际上有一个字面意思“指向数组的指针”(有趣的事实)。从语义上讲,它比数组类型多了一层间接寻址(某种意义上是指向指针的指针)。但从逻辑上讲,指向array N T类型的对象的指针 ->指向数组存储的开头->指向数组中第一个元素的位置。所以:

  char buf[4];
  char (*ptr_buf)[4] = &buf;
  char* ptr_elem0 = &buf[0]; 

ptr_bufptr_elem 具有不同的类型,但具有相同的地址。它们不可相互转换。请参阅此处接受的答案。标准中的这种语言禁止这样的事情:

struct MyStruct {
  char name[4];
  int value;
};

void g(char *chr) {
  char (*name)[4] = reinterpret_cast<char (*)[4]>(chr); // Invalid
  MyStruct* s = reinterpret_cast<MyStruct *>(name);
  // This function uses a pointer to the first element in the array to 
  // access another member of the containing struct. C++ forbids this.
  s->value = 10;
}

void f() {
  MyStruct s;
  g(&s.name[0]);
}

但这种语言和限制在这里无关紧要。这些指针可相互转换的“规则”与访问aligned_storage_t(提供存储的数组)中的第一个非静态元素相关。我们无法从 unsigned char* 相互转换回 unsigned (*) [sizeof(T)] (因为我们甚至没有尝试这样做)。

数组可以转换为指向第一个元素的指针(根据第 7.3.2 节)。指向第一个元素的指针可用于访问整个数组(因为指针算术已定义行为并且数组是连续的)。该指针就是我们需要提供的用于放置新内容的全部内容。请注意,operator new 采用 void*,因此数组到指针的转换无论如何都会发生。如果我们事先自己进行数组到指针的转换,那没有什么区别。

Preface: This is a long answer. Sorry about that! The fundamental answer is short and sweet. But there are a lot of bad arguments out there so there are a lot of basics to touch on.

The proposal claims "Using aligned_* invokes undefined behavior (The types cannot provide storage.)" but provides no additional discussion, proof, or references to language in the standard.

This appears to be a "language-lawyering" position based on an overly literal interpretation of "provides storage" as used in 6.7.2 of the C++ standard. The claim appears to draw from section 6.7.2.3 which states:

If a complete object is created (7.6.2.7) in storage associated with another object e of type “array of N unsigned char” or of type “array of N std::byte” (17.2.1), that array provides storage for the created object ...

The apparent argument then goes on to say, to paraphrase, "Well, aligned_storage_t isn't an array of bytes. It is a struct/union/class that contains an array of bytes. So while that array could provide storage, aligned_storage_t itself cannot." The obvious issue with this argument is... if section 6.7.2.3 doesn't apply here, some other section in the standard still might. There are plenty of other "things" in the standard that "provide storage".

But all of that is irrelevant. What isn't in debate is that the unsigned char array contained within aligned_storage_t can provide storage. We can access that storage if we can get a pointer to it (e.g., for placement new or memcpy of a trivially-copyable type).

And per the standard in the clearest language, we can get a unsigned char* (which points to the beginning of array storage) through the pointer to aligned_storage_t. This is defined behavior to get a pointer to the 'storage' contained inside aligned_storage_t:

std::aligned_storage_t<sizeof(T), alignof(T)> buf;
unsigned char* storage = reinterpret_cast<unsigned char*>(&buf);
T* tptr = new(storage) T;
tptr->~T();

Why? Because aligned_storage_t is implemented either as a union with the unsigned char array as a non-static data member or as standard-layout class (e.g., POD struct) with the array as the first non-static data member.

See section 6.8.2 Compound types of the standard on pages 72-73:

4 Two objects a and b are pointer-interconvertible if:

(4.2) — one is a union object and the other is a non-static data member of that object (11.5), ...

(4.3) — one is a standard-layout class object and the other is the first non-static data member of that object, ...

And the term "pointer-interconvertible" means:

If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a
pointer to one from a pointer to the other via a reinterpret_cast (7.6.1.9). [Note: An array object and its
first element are not pointer-interconvertible, even though they have the same address. — end note]

And that's it. The array contained in aligned_storage_t does provide storage and we can legally get a pointer to that storage through the "pointer-interconvertible" rules.


EDIT: To address the discussion with Language Lawyer in the comments, there is in fact a literal "pointer to an array" (fun fact). Semantically, it has one more level of indirection (in a sense, a pointer to a pointer) than an array type. But logically, the pointer to an object of type array N T -> points to the beginning of array storage -> points to the location of the first element in the array. So:

  char buf[4];
  char (*ptr_buf)[4] = &buf;
  char* ptr_elem0 = &buf[0]; 

ptr_buf and ptr_elem have different types but the same address. They are not interconvertible. See the accepted answer over here. This language in the standard forbids something like this:

struct MyStruct {
  char name[4];
  int value;
};

void g(char *chr) {
  char (*name)[4] = reinterpret_cast<char (*)[4]>(chr); // Invalid
  MyStruct* s = reinterpret_cast<MyStruct *>(name);
  // This function uses a pointer to the first element in the array to 
  // access another member of the containing struct. C++ forbids this.
  s->value = 10;
}

void f() {
  MyStruct s;
  g(&s.name[0]);
}

But that language and restriction is irrelevant here. These pointer-interconvertible "rules" are relevant to get access to first non-static element within aligned_storage_t (the array that provides storage). It doesn't matter that we can't interconvert from unsigned char* back to unsigned (*) [sizeof(T)] (because we aren't even attempting to do that).

An array is convertible to a pointer to the first element (per Section 7.3.2). And that pointer to the first element can be used to access the entire array (because pointer arithmetic has defined behavior and arrays are contiguous). That pointer is all we need to provide to placement new. Note that operator new takes a void* so array-to-pointer conversion will happen anyways. It makes no difference if we do that array-to-pointer conversion ourselves beforehand.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文