Allocator: Add MEM_new_for_free to allow construction of almost-trivial types.

The data constructed by this call remains in the 'C-alloc' realm, i.e.
it can be `MEM_dupallocN`'ed, and `MEM_freeN`'ed.

This is intended as a temporary API only, to facilitate transition to
full C++ handling of data in Blender. It's primary target is to allow
pseudo-POD types to use default values for their members. See e.g.
!134531.

Unlike !143827 and !138829, it does not change the current rule (`new`
must be paired with `delete`, and `alloc` must be paired with `free`).

Instead, it defines an explicit and temporary API to allow a very
limited form of construction to happen on C-allocated data, provided
that the type is default-constructible, and remains trivial after
construction.

### Notes
* The new API is purposely as restrictive as possible, trying to
  only allow the current known needs (init with default member values).
  This can easily be extended if needed.
* To try to stay as close as malloc/calloc behavior as possible, and
  avoid the 'zero-initialization' gotcha, it does not use
  value-initialization, but instead default-initialization on zero-
  initialized memory.
  _Ideally it would even not allow any user-defined default constructor,
  but this does not seem simple to detect._

Pull Request: https://projects.blender.org/blender/blender/pulls/144141
This commit is contained in:
Bastien Montagne
2025-08-11 14:56:11 +02:00
committed by Bastien Montagne
parent 3c3615f3fb
commit 307d0de26e

View File

@@ -340,6 +340,13 @@ void MEM_use_guarded_allocator(void);
*
* \{ */
namespace mem_guarded::internal {
/* Note that we intentionally don't care about a non-trivial default constructor here. */
template<typename T>
constexpr bool is_trivial_after_construction = std::is_trivially_copyable_v<T> &&
std::is_trivially_destructible_v<T>;
} // namespace mem_guarded::internal
/**
* Allocate new memory for an object of type #T, and construct it.
* #MEM_delete must be used to delete the object. Calling #MEM_freeN on it is illegal.
@@ -362,6 +369,46 @@ inline T *MEM_new(const char *allocation_name, Args &&...args)
return new (buffer) T(std::forward<Args>(args)...);
}
/**
* Allocate new memory for an object of type #T, and construct it with its default constructor.
* Both #MEM_delete and #MEM_freeN can be used to delete the object.
*
* Designed to be used with 'pseudo-POD' types, that are trivially copyable and destructible, but
* not trivially constructible. Once constructed, this data can be managed as a C-type one (using
* `MEM_dupallocN`, `MEM_freeN`, safely assigned to a void pointer and freed as such, etc.).
*
* The typical use-cases are C-like structs containing only trivial data, that define default
* values for (some of) their members.
*
* \note This function uses 'default initialization' on zero-initialized memory, _not_ 'value
* initialization'. This means that even if a user-defined default constructor is provided,
* non-explicitely initialized data will be zero-initialized. For POD types (e.g. pure C-style
* structs), its behavior is functionnally identical to using `MEM_callocN<T>()`.
*
* \warning This function is intended as a temporary work-around during the process of converting
* Blender data management from C-style (alloc/free) to C++-style (new/delete). It will be removed
* once not needed anymore (i.e. mainly when there is no more need to dupalloc and free untyped
* data stored in void pointers).
*/
template<typename T> inline T *MEM_new_for_free(const char *allocation_name)
{
static_assert(mem_guarded::internal::is_trivial_after_construction<T>,
"MEM_new_for_free can only construct types that are trivially copyable and "
"destructible, use MEM_new instead.");
void *buffer;
/* There is no lower level #calloc with an alignment parameter, so unless the alignment is less
* than or equal to what we'd get by default, we have to fall back to #memset unfortunately. */
if (alignof(T) <= MEM_MIN_CPP_ALIGNMENT) {
buffer = MEM_callocN(sizeof(T), allocation_name);
}
else {
buffer = mem_guarded::internal::mem_mallocN_aligned_ex(
sizeof(T), alignof(T), allocation_name, mem_guarded::internal::AllocationType::ALLOC_FREE);
memset(buffer, 0, sizeof(T));
}
return new (buffer) T;
}
/**
* Destruct and deallocate an object previously allocated and constructed with #MEM_new, or some
* type-overloaded `new` operators using MEM_guardedalloc as backend.
@@ -564,10 +611,13 @@ template<typename T> inline T *MEM_malloc_arrayN(const size_t length, const char
template<typename T> inline T *MEM_dupallocN(const char *allocation_name, const T &other)
{
# ifdef _MSC_VER
static_assert(std::is_trivially_constructible_v<T>,
"For non-trivial types, MEM_new must be used.");
static_assert(std::is_trivially_assignable_v<T &, T> && std::is_trivially_destructible_v<T>,
"MEM_dupallocN can only duplicate types that are trivially copyable and "
"destructible, use MEM_new instead.");
# else
static_assert(std::is_trivial_v<T>, "For non-trivial types, MEM_new must be used.");
static_assert(mem_guarded::internal::is_trivial_after_construction<T>,
"MEM_dupallocN can only duplicate types that are trivially copyable and "
"destructible, use MEM_new instead.");
# endif
T *new_object = static_cast<T *>(MEM_mallocN_aligned(sizeof(T), alignof(T), allocation_name));
if (new_object) {
@@ -580,9 +630,12 @@ template<typename T> inline void MEM_freeN(T *ptr)
{
# ifdef _MSC_VER
static_assert(std::is_trivially_destructible_v<T>,
"For non-trivial types, MEM_delete must be used.");
"MEM_freeN can only free types that are trivially copyable and destructible, use "
"MEM_delete instead.");
# else
static_assert(std::is_trivial_v<T>, "For non-trivial types, MEM_delete must be used.");
static_assert(mem_guarded::internal::is_trivial_after_construction<T>,
"MEM_freeN can only free types that are trivially copyable and destructible, use "
"MEM_delete instead.");
# endif
mem_guarded::internal::mem_freeN_ex(const_cast<void *>(static_cast<const void *>(ptr)),
mem_guarded::internal::AllocationType::ALLOC_FREE);