Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Edit: Thanks for everyone's answer and replies. Language Lawyer's answer is technically the correct one so that's accepted, but Human-Compiler's answer is the only one that meets the criteria (getting 2+ points) for the bounty, or that is elaborated enough on the question's specific topic.


Full question

Is it defined behavior to have an object b placed in the coroutine state (by e.g. having it as a parameter, or preserving it across a suspension point), where alignof(b) > __STDCPP_DEFAULT_NEW_ALIGNMENT__?

Example:

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();
}

Explanation

When a coroutine is called, heap memory for the coroutine state is allocated via operator new.

This call to operator new may take one of the following forms:

  1. passing all arguments passed to the coroutine following the size requested, or if no such overloads can be found,
  2. passing just the size requested.

Whichever form the call takes, note that it doesn't use the overloads accepting a std::align_val_t, which are necessary to allocate memory that must be aligned more than __STDCPP_DEFAULT_NEW_ALIGNMENT__. Therefore, if an object whose alignment is larger than __STDCPP_DEFAULT_NEW_ALIGNMENT__ must be saved in the coroutine state, there should be no way to guarantee that the object will end up properly aligned in memory.


Experimentation

Godbolt

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

so it definitely doesn't work on GCC trunk (11.0.1 20210307). Replacing 32 with 16 (which equals __STDCPP_DEFAULT_NEW_ALIGNMENT__) eliminates this assertion failure.

godbolt.org cannot run Windows binaries, but the assertion fires with MSVC on my computer as well.

like image 238
fghzxm Avatar asked Mar 09 '21 12:03

fghzxm


1 Answers

From my reading, this would be undefined behavior.

dcl.fct.def.coroutine/9 covers the lookup order for determining the allocation function that will be used should the coroutine need additional storage. The lookup order is quite clear:

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.

(Emphasis mine)

This explicitly mentions that the new overload it will call must start with a std::size_t argument, and may optionally operate on a list of lvalue references p1, p2, ..., pn (if its found in the scope of the promise).

Since in your above example there is no custom operator new defined for the promise type, that means it must select ::operator new(std::size_t) as the overload.

As you already know, ::operator new is only guaranteed to be aligned to __STDCPP_DEFAULT_NEW_ALIGNMENT__ -- which is below the extended alignment required for the coroutine storage. This effectively makes any extended-aligned type in a coroutine be undefined behavior due to misalignment.

Because of how strict the wording is that it must call ::operator new(std::size_t), this should be consistent on any system that implements c++20 correctly. If an implementation chose to support extended-aligned types, it would technically be violating the standard by calling the wrong new overload (which would be an observable deviation).


Judging by the wording on the overload resolution for the allocation function, I think in a case where you require extended-alignment, you should be defining a member-based operator new for your promise that is aware of the possible alignment requirement.

like image 181
Human-Compiler Avatar answered Oct 21 '22 17:10

Human-Compiler