Doc. No.: P0035R3
Date: 2016-05-26
Reply to: Clark Nelson
Phone: +1-503-712-8433
Email: clark.nelson@intel.com
Audience: Core, Library Evolution

Dynamic memory allocation for over-aligned data

Problem statement

To codify widespread existing practice, C++11 added the ability to specify increased alignment (a.k.a. over-alignment) for class types. Unfortunately (but also consistently with existing practice), C++11 did not specify any mechanism by which over-aligned data can be dynamically allocated correctly (i.e. respecting the alignment of the data). For example:

class alignas(16) float4 {
	float f[4];
};
float4 *p = new float4[1000];

In this example, not only is an implementation of C++ not required to allocate properly-aligned memory for the array, for practical purposes it is very nearly required to do the allocation incorrectly. In any event, it is certainly required to perform the allocation by a process that does not take the specified alignment value into account.

This represents a hole in the support for alignment in the language, which really needs to be filled.

History

Significant changes in this revision relative to P0035R1, including all changes in the proposed wording, are enclosed in boxes.

Intel has released a compiler that largely implements the language changes discussed herein, except that, to guarantee backward compatibility, the additional overloads are declared in a new header (<aligned_new>), instead of being predeclared or declared in <new>.

To date, there has not yet been enough experience with the implementation to prove its viability. Nevertheless, it seems appropriate to get this issue back on the committee's radar, so that a decision can be made about it for the C++17 time frame.

Acknowledgements

Pablo Halpern has provided me with much valuable feedback and assistance.

Design considerations

Backward compatibility

One of the first questions that needs to be settled about the future direction is the degree to which backward compatibility with C++11/14 needs to be maintained. On the one hand, in an ideal world, for an example like the one above, it would be obvious that the specified alignment should be honored.

On the other hand, there's no way to achieve that ideal without at least potentially changing the behavior of some programs conforming to an earlier standard. For example, a program might asssume control of dynamic allocation through the use of class-specific operator new and operator delete functions, or by replacing the global functions. These functions don't take any alignment argument. If a different function is used instead, which is somehow passed an alignment value, some degree of backward compatibility is lost.

When backward compatibility and the ideal future direction are in conflict, which should take precedence, and to what degree?

If perfect backward compatibility were required, one way to ensure that might be to require that a new header – say <aligned_new> – be included in order to get new dynamic allocation for over-aligned types. But that would sacrifice convenience and/or correctness; using alignas by itself would presumably never be enough to get correctly aligned dynamic allocation.

Another obvious position to take would be that backward compatibility with C++98, which had no alignment specifier, needs to be complete. This might suggest that dynamic allocation should differ between types involving alignment specifiers and types that don't – which some might consider to be an unfortunate complication.

In C++11/14, when an over-aligned class type has its own dynamic memory allocation functions, it would be reasonable to hope that those functions already do the right thing with respect to alignment, and dangerous to make any change. However, the only way over-alignment could be accommodated by global allocation and deallocation functions would be to replace them with functions that always provide the strictest alignment used by any type in the program. It may be reasonable to assume that very few programs go to that length, instead of using class-specific allocation/deallocation.

Therefore, it may be acceptable to abandon backward compatibility with C++14 with respect to calling a global allocation function for dynamic allocation of an over-aligned type. But if so, that may well be the only acceptable case.

Passing the alignment value

To minimize the possibility of conflict with existing placement allocation functions, it might be advisable to invent a new standard enumeration type to use for alignment parameters; for example:

namespace std {
	enum class align_val_t: size_t;
};
void *operator new(std::size_t, std::align_val_t);	// new overload

It's not clear that this type would need any named constants of its own; it just needs to be able to represent alignment values, which are associated with type size_t. It should perhaps nevertheless be a scoped enumeration, to prevent the possibility that a value of that type would inadvertently be converted to some integer type, and match an existing placement allocation function.

If an allocation function that takes an alignment value is available, it should be used, for the sake of generality; but if no such function is available, a function that doesn't take one should be used, for backward compatibility. This suggests a new rule for new-expressions: attempting to find an allocation function in two phases, with two different sets of arguments.

Class-specific allocation and deallocation

It should be kept in mind that, under the current language rules, any class-specific allocation functions effectively hide all global allocation functions, including the ones in the standard library. For example, the following is invalid:

#include <new>
class X {
	void *operator new(size_t);
	// no operator new(size_t, std::nothrow_t&)
	void operator delete(void *);
};
X *p1 = new X;		// uses X::operator new(size_t)
X *p2 = new(nothrow) X;	// error
// ::operator new(size_t, std::nothrow_t&) is not considered

It is possible to imagine adjusting the rules to enable finding an alignment-aware allocation function more often, but that would also make it more likely that some programmers would write programs believing – incorrectly – that they have taken over complete control of the way that their class is dynamically allocated.

Unified vs. distinct arenas

What implementation techniques should the standard allow for allocation and deallocation of aligned memory?

In POSIX, there is a function named posix_memalign that can allocate over-aligned memory; free is used to free the blocks it allocates.

On Windows, on the other hand, of course malloc, realloc and free are supported for default-aligned memory. In addition, for over-aligned memory, there are functions named _aligned_malloc, _aligned_realloc, and _aligned_free. Memory that's allocated by _aligned_malloc must be freed by _aligned_free, and memory that's allocated by malloc must be freed by free. So logically, there are two disjoint, non-interoperable memory arenas; the program has to know to which arena a block belongs (i.e. how it was allocated) in order to be able to free it.

This is almost certain to be true of any implementation where over-aligned memory allocation is layered on top of “plain old” default-aligned memory allocation. There are probably many such implementations, and they're not likely to go away soon.

In an environment where information about the method used to allocate a block of memory can be lost, having distinct arenas (i.e. distinct deallocation functions) could be inconvenient. A program whose operation depends on the assumption that operator new is equivalent to malloc is effectively an environment where information about the method used to allocate a block of memory is lost.

But in a well-written, portable C++ program, at the point where memory is deallocated, the type of the object being deleted – and therefore whether it is over-aligned – is known. This knowledge could, and probably should, be used to support layered implementations of over-aligned memory allocation.

This implies that, just as a new-expression for an over-aligned type should look for an alignment-aware allocation function, so should a delete-expression for a pointer to an over-aligned type look for an alignment-aware deallocation function. Presumably this would be done by selecting a deallocation function to which the alignment value can be passed, even though probably very few implementations will actually have any use for that value.

Nitty-gritty

For exactly what classes should the allocation method change? Plausible answers include:

  1. Those affected by an alignas that actually specifies over-alignment.
  2. Those affected by an explicit alignas, even if the alignment value is basic (i.e. small).

The first answer seems to be right from a pragmatic perspective, but one consequence is that the behavior of a program might depend (in a new way) on an implementation-defined parameter. But if the only difference between alignment-aware and alignment-unaware allocation/deallocation functions is the actual allocation mechanism (i.e., in a well-designed program), this should not be a problem. It's rather like the implementation's license to elide certain copies, which implies that a copy constructor had really better just make a copy.

The below WD changes use the first answer, through use of “over-aligned”. The Intel implementation uses the first answer by default, but has a command-line option to select the second answer, for the sake of experimentation.

Also, it should be noted that the over-alignment threshold used by the Intel implementation doesn't exactly match the standard's definition of basic alignment. The threshold used is actually the alignment observed to be guaranteed by the implementation of malloc for the target environment. (In all of the environments tested, this turned out to be twice the size of a pointer.)

Assuming the existence of a variety of allocation functions, which one should be used for an over-aligned allocation? I believe the answer should be the first one from the following list that is known to exist:

  1. class-specific and alignment-aware
  2. class-specific and alignment-unaware
  3. global and alignment-aware
  4. [global and alignment-unaware]

It makes sense for a class-specific, alignment-unaware allocation function to be preferred over one that is global and alignment-aware, because there are many cases where a class-specific allocation function has enough information, even without an explicit parameter, to do the allocation with sufficient alignment. (Likely exceptions include a template class with a base or member of a type that is a template parameter, and a derived class that inherits its allocation function from a base class, and also adds a member or base of over-aligned type.)

If a global, alignment-aware allocation function is predeclared, then it will never be necessary to use a global, alignment-unaware allocation function for an over-aligned type; hence the brackets around item 4.

It should be noted that an alignment-aware allocation function would be perfectly capable of performing an alignment that would suffice for an alignment-unaware function. In other words, from some perspective, it would make sense to let operator new(size_t) call operator new(size_t, align_val_t), filling in the alignment value that it feels it needs to satisfy, and move the allocation loop to the alignment-aware function. But that would be pretty novel, so I have chosen not to propose it.

The allocation alignment threshold

At a joint EWG/CWG session in Jacksonville, it was agreed that the threshold above which to use the new allocation routines should be the maximum alignment provided by the existing allocation routines, and that this value should be made available to the programmer.

In this revision, I have chosen to use a predefined macro for this purpose.

Unquestionably, the compiler needs to know this value. It's possible to imagine some magical way it could be learned from some library header, but it needs to be known even if no header has been included. And given the existence of independent implementations of the standard C++ library, pragmatically sometimes it will be the programmer who needs to inform the compiler of this value at build time. To me, this set of requirements really suggests something an awful lot like a predefined macro.

There is no reason why this value couldn't also be exposed (with a “nicer” name) by something in a library header. But I'm afraid that in real-world implementations, the actual value from the library would be provided by some macro anyway.

So really, the only question is whether the predefined macro name appears in the standard, or is left for implementations to pick. And I think library implementers would be grateful if they didn't have to deal with different predefined macro names for different compilers.

LEWG will eventually consider whether an additional non-macro name for this is justified. The absence of any such proposal in this revision should be taken as a sign of caution (or laziness) on my part, and not of prejudice or opposition.

Proposed working draft changes

The following changes are relative to N4582 (post-Jacksonville).

There is one change of terminology worth noting. Today, the phrase “placement new” is ambiguous. In some contexts it means adding arguments to a call to an allocation function, with any types and unspecified purpose. In other contexts, it is used to refer specifically to cases where there is a single additional argument of type void *, in which case the allocation function doesn't actually allocate anything. I refer to the latter cases as “non-allocating”, and refer to “allocating” cases to distinguish them when necessary.

Change 3.7.4p1:

Objects can be created dynamically during program execution (1.9), using new-expressions (5.3.4), and destroyed using delete-expressions (5.3.5). A C++ implementation provides access to, and management of, dynamic storage via the global allocation functions operator new and operator new[] and the global deallocation functions operator delete and operator delete[]. [ Note: The non-allocating forms described in [new.delete.placement] do not perform allocation or deallocation. —end note]

This is intended as a clarification of what seems already to be implied by [new.delete.placement]:

The provisions of (3.7.4) do not apply to these reserved placement forms of operator new and operator delete.

Change 3.7.4p2:

The library provides default definitions for the global allocation and deallocation functions. Some global allocation and deallocation functions are replaceable (18.6.1). A C++ program shall provide at most one definition of a replaceable allocation or deallocation function. Any such function definition replaces the default version provided in the library (17.6.4.6). The following allocation and deallocation functions (18.6) are implicitly declared in global scope in each translation unit of a program.

void* operator new(std::size_t);
void* operator new(std::size_t, std::align_val_t);
void* operator new[](std::size_t);
void* operator new[](std::size_t, std::align_val_t);
void operator delete(void*);
void operator delete(void*, std::align_val_t);
void operator delete[](void*);
void operator delete[](void*, std::align_val_t);
void operator delete(void*, std::size_t);
void operator delete(void*, std::align_val_t, std::size_t);
void operator delete[](void*, std::size_t);
void operator delete[](void*, std::align_val_t, std::size_t);

These implicit declarations introduce only the function names operator new, operator new[], operator delete, and operator delete[]. [ Note: The implicit declarations do not introduce the names std, std::size_t, std::align_val_t, or any other names that the library uses to declare these names. Thus, a new-expression, delete-expression or function call that refers to one of these functions without including the header <new> is well-formed. However, referring to std or std::size_t or std::align_val_t is ill-formed unless the name has been declared by including the appropriate header. —end note] Allocation and/or deallocation functions can may also be declared and defined for any class (12.5).

The set of added functions is unchanged, but it was previously presented as a single cluster; the difference is editorial, but this ordering probably makes more sense. OTOH, a different order may make even more sense; see 18.6.1.

For deallocation, I originally chose to put the alignment before the size because the size argument is necessarily just a hint, whereas if separate arenas are used, at least the presence or absence of an alignment argument is significant for correctness. In a CWG teleconference, a mild consensus preference was expressed that when a deallocation function takes both an alignment and a size, the size should precede the alignment, mainly because that's the order when an allocation function takes both. LEWG needs to decide which order it prefers.

Change 3.7.4.1p2:

The allocation function attempts to allocate the requested amount of storage. If it is successful, it shall return the address of the start of a block of storage whose length in bytes shall be at least as large as the requested size. There are no constraints on the contents of the allocated storage on return from the allocation function. The order, contiguity, and initial value of storage allocated by successive calls to an allocation function are unspecified. The pointer returned shall be suitably aligned so that it can be converted to a pointer of to any suitable complete object type ([new.delete.single]) with a fundamental alignment requirement (3.11) and then used to access the object or array in the storage allocated (until the storage is explicitly deallocated by a call to a corresponding deallocation function). Even if the size of the space requested is zero, the request can fail. If the request succeeds, the value returned shall be a non-null pointer value (4.10) p0 different from any previously returned value p1, unless that value p1 was subsequently passed to an operator delete. Furthermore, for the library allocation functions in 18.6.2.1 and 18.6.2.2, p0 shall point to a block of storage disjoint from the storage for any other object accessible to the caller. The effect of indirecting through a pointer returned as a request for zero size is undefined.36

The text here is intentionally made less specific, with the details to be found elsewhere.

Change 3.7.4.2p2:

Each deallocation function shall return void and its first parameter shall be void*. A deallocation function can may have more than one parameter. The global operator delete with exactly one parameter is a usual (nonplacement) deallocation function. The global operator delete with exactly two parameters, the second of which has type std::size_t, is a usual deallocation function. Similarly, the global operator delete[] with exactly one parameter is a usual deallocation function. The global operator delete[] with exactly two parameters, the second of which has type std::size_t, is a usual deallocation function.37 If a class T has a member deallocation function named operator delete with exactly one parameter, then that function is a usual deallocation function. If class T does not declare such an operator delete but does declare a member deallocation function named operator delete with exactly two parameters, the second of which has type std::size_t, then this function is a usual deallocation function. Similarly, if a class T has a member deallocation function named operator delete[] with exactly one parameter, then that function is a usual (non-placement) deallocation function. If class T does not declare such an operator delete[] but does declare a member deallocation function named operator delete[] with exactly two parameters, the second of which has type std::size_t, then this function is a usual deallocation function. A usual deallocation function is a deallocation function that has:

Footnote: 37) This deallocation function The global operator delete(void*, std::size_t) precludes use of an allocation function void operator new(std::size_t, std::size_t) as a placement allocation function (C.3.2).

A deallocation function can may be an instance of a function template. Neither the first parameter nor the return type shall depend on a template parameter. [ Note: That is, a deallocation function template shall have a first parameter of type void* and a return type of void (as specified above). —end note ] A deallocation function template shall have two or more function parameters. A template instance is never a usual deallocation function, regardless of its signature.

Adding the new overloads to the set of "usual" deallocation functions in the same style as the previous formulation would have required outrageous verbosity. My new formulation is much more concise and (to my eyes) comprehensible, but there is a technical difference worth noting.

Previously, in class scope, if deallocation functions with and without a size parameter are both declared, the one with the size parameter was not "usual". My simpler formulation includes both overloads. But it is not difficult to tweak the selection algorithm (5.3.5p10) to produce the same result as previously.

Change 3.7.4.2p3:

If a deallocation function terminates by throwing an exception, the behavior is undefined. The value of the first argument supplied to a deallocation function may be a null pointer value; if so, and if the deallocation function is one supplied in the standard library, the call has no effect. Otherwise, the behavior is undefined if the value supplied to operator delete(void*) in the standard library is not one of the values returned by a previous invocation of either operator new(std::size_t) or operator new(std::size_t, const std::nothrow_t&) in the standard library, and the behavior is undefined if the value supplied to operator delete[](void*) in the standard library is not one of the values returned by a previous invocation of either operator new[](std::size_t) or operator new[](std::size_t, const std::nothrow_t&) in the standard library.

These requirements apply only to the library implementations, and are already stated in 18.6.

Change 3.7.4.3p2:

A pointer value is a safely-derived pointer to a dynamic object only if it has an object pointer type and it is one of the following:

Change 3.9.2p3:

... [ Note: Pointers to over-aligned types (3.11) have no special representation, but their range of valid values is restricted by the extended alignment requirement. This International Standard specifies only two ways of obtaining such a pointer: taking the address of a valid object with an over-aligned type, and using one of the runtime pointer alignment functions. An implementation may provide other means of obtaining a valid pointer value for an over-aligned type.end note ]

Change 3.11p3:

An extended alignment is represented by an alignment greater than alignof(std::max_align_t). It is implementation-defined whether any extended alignments are supported and the contexts in which they are supported (7.6.2). A type having an extended alignment requirement is an over-aligned type. A new-extended alignment is represented by an alignment greater than __STDCPP_DEFAULT_NEW_ALIGNMENT__. [ Note: every over-aligned type is or contains a class type to which extended alignment applies (possibly through a non-static data member). —end note ]

Change 5.3.4p1:

The new-expression attempts to create an object of the type-id (8.1) or new-type-id to which it is applied. The type of that object is the allocated type. This type shall be a complete object type, but not an abstract class type or array thereof (1.8, 3.9, 10.4). It is implementation-defined whether over-aligned types are supported (3.11). [ Note: ...

Change 5.3.4p8:

A new-expression may obtain storage for the object by calling an allocation function (3.7.4.1). If the new-expression terminates by throwing an exception, it may release storage by calling a deallocation function (3.7.4.2). If the allocated type is a non-array type, the allocation function's name is operator new and the deallocation function's name is operator delete. If the allocated type is an array type, the allocation function's name is operator new[] and the deallocation function's name is operator delete[]. [ Note: an implementation shall provide default definitions for the global allocation functions (3.7.4, 18.6.2.1, 18.6.2.2). A C++ program can provide alternative definitions of these functions (17.6.4.6) and/or class-specific versions (12.5). The set of allocation and deallocation functions that may be called by a new-expression may include functions that do not perform allocation or deallocation; for example, see [new.delete.placement].end note ]

As an editorial matter, note that the current WD inappropriately uses italics for the first occurrence of “allocation function” in this paragraph; it should be removed.

Change 5.3.4p13:

The new-placement syntax is used to supply additional arguments to an allocation function; such an expression is called a placement new-expression. If used, overload Overload resolution is performed on a function call created by assembling an argument list. consisting of The first argument is the amount of space requested (the first argument) , and has type std::size_t. If the type of the allocated object has new-extended alignment, the next argument is the type's alignment, and has type std::align_val_t. and the If the new-placement syntax is used, its expressions in the new-placement part of the new-expression ( are the second and succeeding arguments). The first of these arguments has type std::size_t and the remaining arguments have the corresponding types of the expressions in the new-placement; such an expression is called a placement new-expression. If no matching function is found and the allocated object type has new-extended alignment, the alignment argument is removed from the argument list, and overload resolution is performed again.

Change 5.3.4p14:

[ Example:

Here, each instance of x and y are is a non-negative unspecified values value representing array allocation overhead; ... — end example ]

Change 5.3.4p15:

[ Note: unless an allocation function has a non-throwing exception specification (15.4), it indicates failure to allocate storage by throwing a std::bad_alloc exception (3.7.4.1, Clause 15, 18.6.3.1); it returns a non-null pointer otherwise. If the allocation function has a non-throwing exception specification, it returns null to indicate failure to allocate storage and a non-null pointer otherwise. —end note ] If the allocation function is a reserved placement allocation function non-allocating form (18.6.2.3) that returns null, the behavior is undefined. Otherwise, if the allocation function returns null, initialization shall not be done, the deallocation function shall not be called, and the value of the new-expression shall be null.

Change 5.3.4p22:

A declaration of a placement deallocation function matches the declaration of a placement allocation function if it has the same number of parameters and, after parameter transformations (8.3.5), all parameter types except the first are identical. If the lookup finds a single matching deallocation function, that function will be called; otherwise, no deallocation function will be called. If the lookup finds the two-parameter form of a usual deallocation function with a size parameter (3.7.4.2) and that function, considered as a placement deallocation function, would have been selected as a match for the allocation function, the program is ill-formed. For a non-placement allocation function, the normal deallocation function lookup is used to find the matching deallocation function (5.3.5) [ Example:

Change 5.3.5p5:

If the object being deleted has incomplete class type at the point of deletion, and the complete class has new-extended alignment, has a non-trivial destructor, or has a deallocation function, the behavior is undefined.

Change 5.3.5p10:

If deallocation function lookup finds both a more than one usual deallocation function with only a pointer parameter and a usual deallocation function with both a pointer parameter and a size parameter, the function to be called is selected as follows:

Change 5.3.5p11:

When a delete-expression is executed, the selected deallocation function shall be called with the address of the block of storage to be reclaimed as its first argument. If a deallocation function with an alignment parameter is used, the alignment of the type of the object to be deleted is passed as the corresponding argument. and (if the two-parameter If a deallocation function with a size parameter is used), the size of the block is passed as its second the corresponding argument.82

Add a new predefined macro to the list in 16.8p1:

__STDCPP_DEFAULT_NEW_ALIGNMENT__
An integer literal whose value is the threshold for an alignment value above which operator new(std::size_t)is not guaranteed to allocate appropriately-aligned memory.

Change 17.6.4.6p2:

A C++ program may provide the definition for any of the following dynamic memory allocation function signatures declared in header <new> (3.7.4, 18.6):

Change 18.6.1:

namespace std {
    class bad_alloc;
    class bad_array_new_length;
    enum class align_val_t: size_t {};
    struct nothrow_t {};
    extern const nothrow_t nothrow;
    typedef void (*new_handler)();
    new_handler get_new_handler() noexcept;
    new_handler set_new_handler(new_handler new_p) noexcept;

    // 18.6.4, hardware interference size
    static constexpr size_t hardware_destructive_interference_size =
       implementation-defined ;
    static constexpr size_t hardware_constructive_interference_size =
       implementation-defined ;
};
void* operator new(std::size_t size);
void* operator new(std::size_t size, std::align_val_t alignment);
void* operator new(std::size_t size, const std::nothrow_t&) noexcept;
void* operator new(std::size_t size, std::align_val_t alignment,
			const std::nothrow_t&) noexcept;
void operator delete(void* ptr) noexcept;
void operator delete(void* ptr, std::size_t size) noexcept;
void operator delete(void* ptr, std::align_val_t alignment) noexcept;
void operator delete(void* ptr, std::align_val_t alignment,
			std::size_t size) noexcept;
void operator delete(void* ptr, const std::nothrow_t&) noexcept;
void operator delete(void* ptr, std::align_val_t alignment,
			const std::nothrow_t&) noexcept;
void* operator new[](std::size_t size);
void* operator new[](std::size_t size, std::align_val_t alignment);
void* operator new[](std::size_t size, const std::nothrow_t&) noexcept;
void* operator new[](std::size_t size, std::align_val_t alignment,
			const std::nothrow_t&) noexcept;
void operator delete[](void* ptr) noexcept;
void operator delete[](void* ptr, std::size_t size) noexcept;
void operator delete[](void* ptr, std::align_val_t alignment) noexcept;
void operator delete[](void* ptr, std::align_val_t alignment,
			std::size_t size) noexcept;
void operator delete[](void* ptr, const std::nothrow_t&) noexcept;
void operator delete[](void* ptr, std::align_val_t alignment,
			const std::nothrow_t&) noexcept;
void* operator new  (std::size_t size, void* ptr) noexcept;
void* operator new[](std::size_t size, void* ptr) noexcept;
void operator delete  (void* ptr, void*) noexcept;
void operator delete[](void* ptr, void*) noexcept;

I have carefully added the new overloads in the synopsis to appear in the same order that they are described below. Unfortunately, that required moving the declarations of nothrow deallocation functions; that's an editorial change that I didn't explicitly mark above. I also noticed that the lists in 3.7.4 and 17.6.4.6 are ordered differently than this one, and for that matter that the list in 17.6.4.6 has bullets; editorially, it may be desirable for these lists to be more consistent with one another.

Change 18.6.2p1:

Except where otherwise specified, the provisions of (3.7.4) apply to the library versions of operator new and operator delete. If the value of an alignment argument passed to any of these functions is not a valid alignment value, or for an allocation function if the size argument is not an integral multiple of the alignment argument, the behavior is undefined.

The provision about the size being a multiple of the alignment was added in response to a comment made at the Jacksonville meeting. However, Martin Sebor, who has also been working on aligned allocation for C, points out that it unnecessarily complicates the following technique:

typedef struct S {
    _Alignas(64) int a[16];
    char x[];
} S;

S *s = aligned_alloc(_Alignof(S), sizeof(S) + 20);

It basically assumes that the only way a block of allocated memory can be used is by filling it with an array (of elements all having the same type). But raw memory is (or at least ought to be) more flexible than that. So I think it would make sense to remove that provision, or change it such that the size must be at least as great as the alignment.

Change section 18.6.2.1 as follows. (Some technically irrelevant editorial improvements are included; in CWG, these would be called “en passant” or “drive-by” changes. Also, there are some inappropriate uses of italics in these two sections which should be fixed, for phrases including “allocation function”, “deallocation function”, and “ptr”.)

18.6.2.1 Single-object forms

void* operator new(std::size_t size);
void* operator new(std::size_t size, std::align_val_t alignment);

Effects: The allocation function functions ([basic.stc.dynamic.allocation]) called by a new-expression ([expr.new]) to allocate size bytes of storage. The second form is called for a type with new-extended alignment, and allocates storage with the specified alignment. The first form is called otherwise, and allocates storage suitably aligned to represent any object of that size or smaller, provided the object's type does not have new-extended alignment.

Replaceable: a C++ program may define a function with this function signature that displaces functions with either of these function signatures, and thereby displace the default version versions defined by the C++ standard library.

Required behavior: Return a non-null pointer to suitably aligned storage ([basic.stc.dynamic]), or else throw a bad_alloc exception. This requirement is binding on a any replacement version versions of this function these functions.

Default behavior:

void* operator new(std::size_t size, const std::nothrow_t&) noexcept;
void* operator new(std::size_t size, std::align_val_t alignment, const std::nothrow_t&) noexcept;

Effects: Same as above, except that it is these are called by a placement version of a new-expression when a C++ program prefers a null pointer result as an error indication, instead of a bad_alloc exception.

Replaceable: a C++ program may define a function with this function signature that displaces the default version defined by the C++ standard library. This should be changed similarly to paragraph 2.

Required behavior: Return a non-null pointer to suitably aligned storage ([basic.stc.dynamic]), or else return a null pointer. This Each of these nothrow version versions of operator new returns a pointer obtained as if acquired from the (possibly replaced) ordinary version corresponding non-placement function. This requirement is binding on a replacement version of this function. The last sentence should be changed similarly to paragraph 3.

Default behavior: Calls operator new(size), or operator new(size, alignment), respectively. If the call returns normally, returns the result of that call. Otherwise, returns a null pointer.

[ Example:

T* p1 = new T; // throws bad_alloc if it fails
T* p2 = new(nothrow) T; // returns nullptr if it fails

end example ]

void operator delete(void* ptr) noexcept;
void operator delete(void* ptr, std::size_t size) noexcept;
void operator delete(void* ptr, std::align_val_t alignment) noexcept;
void operator delete(void* ptr, std::align_val_t alignment, std::size_t size) noexcept;

Effects: The deallocation function functions ([basic.stc.dynamic.deallocation]) called by a delete-expression to render the value of ptr invalid.

Replaceable: a C++ program may define a function with signature void operator delete(void* ptr) noexcept that displaces functions with any of these signatures, and thereby displace the default version(s) defined by the C++ standard library. If this a function (without a size parameter) is defined, the program should also define void operator delete(void* ptr, td::size_t size) noexcept the corresponding function with a size parameter. If this a function with a size parameter is defined, the program shall also define the corresponding version without the size parameter. [ Note: The default behavior below may change in the future, which will require replacing both deallocation functions when replacing the allocation function. —end note ]

Requires: ptr shall be a null pointer or its value shall be a value returned point to a block of memory allocated by an earlier call to the a (possibly replaced) operator new(std::size_t) or operator new(std::size_t, const std::nothrow_t& std::align_val_t) which has not been invalidated by an intervening call to operator delete(void*) or operator delete(void*, std::size_t).

Requires: If an implementation has strict pointer safety ([basic.stc.dynamic.safety]) then ptr shall be a safely-derived pointer.

Requires: If the alignment parameter is not present, ptr shall have been returned by an allocation function not taking an alignment argument. If present, the alignment argument shall equal the alignment argument passed to the allocation function that returned ptr. If present, the std::size_t size argument shall equal the size argument passed to the allocation function that returned ptr.

Required behavior: Calls A call to an operator delete(void* ptr, std::size_t size) taking a size may be changed to calls a call to the corresponding operator delete(void* ptr) not taking a size, without affecting memory allocation. [ Note: A conforming implementation is for operator delete(void* ptr, std::size_t size) to simply call operator delete(ptr). —end note ]

Default behavior: the function operator delete(void* ptr, std::size_t size) calls operator delete(ptr). The function operator delete(void* ptr, std::align_val_t alignment, std::size_t) calls operator delete(ptr, alignment). [ Note: See the note in the above Replaceable paragraph. —end note ]

Default behavior: If ptr is null, does nothing. Otherwise, reclaims the storage allocated by the earlier call to operator new.

The previous revision suggested changing “Default behavior” to “Required behavior”. I am walking back from that suggestion, not because I think it was wrong, but simply because it has nothing to do with alignment. If that should be done, let it be done as a separate effort.

Remarks: It is unspecified under what conditions part or all of such reclaimed storage will be allocated by subsequent calls to operator new or any of aligned_alloc, calloc, malloc, or realloc, declared in <cstdlib>.

void operator delete(void* ptr, const std::nothrow_t&) noexcept;
void operator delete(void* ptr, std::align_val_t alignment, const std::nothrow_t&) noexcept;

Effects: The deallocation function functions ([basic.stc.dynamic.deallocation]) called by the implementation to render the value of ptr invalid when the constructor invoked from a nothrow placement version of the new-expression throws an exception.

Replaceable: a C++ program may define a function functions with signature void operator delete(void* ptr, const std::nothrow_t&) noexcept that displaces any of these signatures, and thereby displace the default version(s) defined by the C++ standard library.

Requires: If an implementation has strict pointer safety ([basic.stc.dynamic.safety]) then ptr shall be a safely-derived pointer.

Default behavior: The function operator delete(void* ptr, const std::nothrow_t&) calls operator delete(ptr). The function operator delete(void* ptr, std::align_val_t alignment, const std::nothrow_t&) calls operator delete(ptr, alignment).

Change section 18.6.2.2:

18.6.2.2 Array forms

void* operator new[](std::size_t size);
void* operator new[](std::size_t size, std::align_val_t alignment);

Effects: The allocation function functions ([basic.stc.dynamic.allocation]) called by the array form of a new-expression ([expr.new]) to allocate size bytes of storage. The second form is called for a type with new-extended alignment, and allocates storage with the specifed alignment. The first form is called otherwise, and allocates storage suitably aligned to represent any array object of that size or smaller, provided the array's element type does not have new-extended alignment.224

Footnote 224: It is not the direct responsibility of operator new[](std::size_t) or operator delete[](void*) to note the repetition count or element size of the array. Those operations are performed elsewhere in the array new and delete expressions. The array new expression, may, however, increase the size argument to operator new[](std::size_t) to obtain space to store supplemental information.

Replaceable: a C++ program can may define a function with this function signature that displaces the default version defined by the C++ standard library. This should be changed similarly to paragraph 2 above.

Required behavior: Same as for operator new(std::size_t) the corresponding single-object forms. This requirement is binding on a replacement version of this function. The last sentence should be changed similarly to paragraph 3 above.

Default behavior: Returns operator new(size), or operator new(size, alignment), respectively.

void* operator new[](std::size_t size, const std::nothrow_t&) noexcept;
void* operator new[](std::size_t size, std::align_val_t alignment, const std::nothrow_t&) noexcept;

Effects: Same as above, except that it is these are called by a placement version of a new-expression when a C++ program prefers a null pointer result as an error indication, instead of a bad_alloc exception.

Replaceable: a C++ program can may define a function with this function signature that displaces the default version defined by the C++ standard library. This should be changed similarly to paragraph 2.

Required behavior: Return a non-null pointer to suitably aligned storage ([basic.stc.dynamic]), or else return a null pointer. This Each of these nothrow version versions of operator new[] returns a pointer obtained as if acquired from the (possibly replaced) operator new[](std::size_t) function corresponding non-placement function. This requirement is binding on a replacement version of this function. The last sentence should be changed similarly to paragraph 3.

Default behavior: Calls operator new[](size), or operator new[](size, alignment), respectively. If the call returns normally, returns the result of that call. Otherwise, returns a null pointer.

void operator delete[](void* ptr) noexcept;
void operator delete[](void* ptr, std::size_t size) noexcept;
void operator delete[](void* ptr, std::align_val_t alignment) noexcept;
void operator delete[](void* ptr, std::align_val_t alignment, std::size_t size) noexcept;

Effects: The deallocation function functions ([basic.stc.dynamic.deallocation]) called by the array form of a delete-expression to render the value of ptr invalid.

Replaceable: a C++ program can may define a function with signature void operator delete[](void* ptr) noexcept that displaces functions with any of these signatures, and thereby displace the default version(s) defined by the C++ standard library. If this a function (without a size parameter) is defined, the program should also define void operator delete[](void* ptr, std::size_t size) noexcept the corresponding function with a size parameter. If this a function with a size parameter is defined, the program shall also define the corresponding version without the size parameter. [ Note: The default behavior below may change in the future, which will require replacing both deallocation functions when replacing the allocation function. —end note ]

Requires: ptr shall be a null pointer or its value shall be the value returned point to a block of memory allocated by an earlier call to a (possibly replaced) operator new[](std::size_t) or operator new[](std::size_t, const std::nothrow_t& std::align_val_t) which has not been invalidated by an intervening call to operator delete[](void*) or operator delete[](void*, std::size_t).

Requires: If the alignment parameter is not present, ptr shall have been returned by an allocation function not taking an alignment argument. If present, the alignment argument shall equal the alignment argument passed to the allocation function that returned ptr. If present, the std::size_t size argument must shall equal the size argument passed to the allocation function that returned ptr.

Required behavior: Calls A call to operator delete[](void* ptr, std::size_t size) taking a size may be changed to calls a call to operator delete[](void* ptr) not taking a size, without affecting memory allocation. [ Note: A conforming implementation is for operator delete[](void* ptr, std::size_t size) to simply call operator delete[](void* ptr). —end note ]

Requires: If an implementation has strict pointer safety ([basic.stc.dynamic.safety]) then ptr shall be a safely-derived pointer.

Default behavior: operator delete[](void* ptr, std::size_t size) calls operator delete[](ptr), and operator delete[](void* ptr) calls operator delete(ptr). The functions that have a size parameter forward their other parameters to the corresponding function without a size parameter. The functions that do not have a size parameter forward their parameters to the corresponding operator delete (single-object) function.

This is the one place in the standard where the default behavior needs to be specified for four replaceable functions. My senses of esthetics and propriety rebelled at the prospect of enumerating them all, especially in a single flowing paragraph.

void operator delete[](void* ptr, const std::nothrow_t&) noexcept;
void operator delete[](void* ptr, std::align_val_t alignment, const std::nothrow_t&) noexcept;

Effects: The deallocation function functions ([basic.stc.dynamic.deallocation]) called by the implementation to render the value of ptr invalid when the constructor invoked from a nothrow placement version of the array new-expression throws an exception.

Replaceable: a C++ program may define a function functions with signature void operator delete[](void* ptr, const std::nothrow_t&) noexcept that displaces any of these signatures, and thereby displace the default version(s) defined by the C++ standard library.

Requires: If an implementation has strict pointer safety ([basic.stc.dynamic.safety]) then ptr shall be a safely-derived pointer.

Default behavior: The function operator delete[](void* ptr, const std::nothrow_t&) calls operator delete[](ptr). The function operator delete[](void* ptr, std::align_val_t alignment, const std::nothrow_t&) calls operator delete[](ptr, alignment).

Change the title of section 18.6.2.3:

18.6.2.3 Placement Non-allocating forms [new.delete.placement]

Change 20.9.9.1p5:

Returns: A pointer to the initial element of an array of storage of size n * sizeof(T), aligned appropriately for objects of type T. It is implementation-defined whether over-aligned types are supported (3.11).

Change 20.9.9.1p6:

Remark: the storage is obtained by calling ::operator new(std::size_t) (18.6.2), but it is unspecified when or how often this function is called. The use of hint is unspecified, but intended as an aid to locality if an implementation so desires.

Change 20.9.9.1p10:

Remarks: Uses ::operator delete(void*, std::size_t) (18.6.2), but it is unspecified when this function is called.

Change 20.9.11p1:

Effects: Obtains a pointer to uninitialized, contiguous storage for N adjacent objects of type T, for some non-negative number N. It is implementation-defined whether over-aligned types are supported (3.11).