将异常对齐的对象置于协程状态是否定义了行为?

Is it defined behavior to place exotically aligned objects in the coroutine state?

编辑:感谢大家的回答和回复。 Language Lawyer 的答案在技术上是正确的,因此被接受,但 Human-Compiler 的答案是唯一符合赏金标准(获得 2+ 分),或者对问题的特定主题进行了充分阐述的答案。


完整问题

是否定义了对象的行为b 置于协程状态 (例如,将其作为参数, 或将其保存在悬挂点上), alignof(b) > __STDCPP_DEFAULT_NEW_ALIGNMENT__?

示例:

inline constexpr size_t large_alignment =
    __STDCPP_DEFAULT_NEW_ALIGNMENT__ * 2;

struct alignas(large_alignment) behemoth {
  void attack();
  unsigned char data[large_alignment];
};

task<void> invade(task_queue &q) {
  behemoth b{};
  co_await submit_to(q);
  b.attack();
}

说明

调用协程时, 协程状态的堆内存 通过 operator new.

分配

这次调用 operator new 可以采用以下形式之一:

  1. 传递所有传递给协程的参数 按照要求的尺寸, 或者如果找不到这样的重载,
  2. 只传递请求的大小。

无论调用哪种形式, 请注意,它不使用重载 接受 std::align_val_t, 这是分配内存所必需的 必须对齐超过 __STDCPP_DEFAULT_NEW_ALIGNMENT__。 因此,如果一个对象的对齐方式 大于 __STDCPP_DEFAULT_NEW_ALIGNMENT__ 必须保存在协程状态, 应该没有办法保证 该对象最终将正确对齐 在记忆中。


实验

Godbolt

async f(): Assertion `reinterpret_cast<uintptr_t>(&b) % 32ull == 0' failed.

所以它绝对不适用于 GCC t运行k (11.0.1 20210307)。 将 32 替换为 16(等于 __STDCPP_DEFAULT_NEW_ALIGNMENT__) 消除了这个断言失败。

godbolt.org 不能 运行 Windows 二进制文件, 但是我的计算机上的 MSVC 也会触发断言。

根据我的阅读,这将是未定义的行为。

dcl.fct.def.coroutine/9 涵盖用于确定在协程需要额外存储时将使用的分配函数的查找顺序。查找顺序很清楚:

An implementation may need to allocate additional storage for a coroutine. This storage is known as the coroutine state and is obtained by calling a non-array allocation function ([basic.stc.dynamic.allocation]).

The allocation function's name is looked up in the scope of the promise type. If this lookup fails, the allocation function's name is looked up in the global scope. If the lookup finds an allocation function in the scope of the promise type, overload resolution is performed on a function call created by assembling an argument list. The first argument is the amount of space requested, and has type std​::​size_­t. The lvalues p1pn are the succeeding arguments.

If no viable function is found ([over.match.viable]), overload resolution is performed again on a function call created by passing just the amount of space required as an argument of type std​::​size_­t.

(强调我的)

这明确提到它将调用的 new 重载必须以 std::size_t 参数开头,并且可以选择对左值引用列表 p1p2, ..., pn(如果在承诺范围内找到)。

由于在上面的示例中没有为承诺类型定义自定义 operator new,这意味着它必须 select ::operator new(std::size_t) 作为重载。

如您所知,::operator new 只能保证与 __STDCPP_DEFAULT_NEW_ALIGNMENT__ 对齐——这低于协程存储所需的扩展对齐。由于未对齐,这有效地使协程中的任何扩展对齐类型成为未定义行为

由于必须调用 ::operator new(std::size_t) 的措辞多么严格,这在任何正确实现 c++20 的系统上应该是一致的。如果一个实现选择支持扩展对齐类型,它在技术上会通过调用错误的 new 重载(这将是一个可观察到的偏差)来违反标准。


从分配函数重载决议的措辞来看,我认为在你需要扩展对齐的情况下,你应该为你的承诺定义一个基于成员的 operator new可能的对齐要求。

来自[basic.align]/3

It is implementation-defined whether any extended alignments are supported and the contexts in which they are supported.

所以答案是:实现定义的。这句话的第二部分听起来可能是一个实现可能支持在普通函数中创建过度对齐类型的对象,但在协程中不支持。

[basic.align]/9:

If a request for a specific extended alignment in a specific context is not supported by an implementation, the program is ill-formed.

不支持扩展对齐但不发出诊断消息的实现不符合要求。

简单回顾一下这个问题到目前为止收到的回复:

  1. 正如@LanguageLawyer 所指出的,编译器没有义务在任何上下文中支持扩展对齐类型,并且允许这些类型的支持程度因上下文而异。因此,符合编译器不保证b正确对齐。

  2. 即使标准要求始终使用 operator new 的非扩展对齐版本,编译器仍然可以通过过度分配设法将协程状态置于正确对齐,并在 运行 时间选择一个充分对齐的地址。但同样,从技术上讲,它 不必 支持任何东西。

  3. 在今天的现状下,OP 的代码示例肯定是不正确的,尽管从更严格的意义上讲,它只是在 运行 时间 b 实际上是未定义的行为放错地方并导致某种形式的未对齐访问(UB 是 运行 时间 属性)。