I am working on a version of std::unique_ptr
and std::make_unique
for aligned memory. The purpose of this is vectorization, e.g., SSE or AVX, which has higher alignment requirements than the underlying types.
My header providing aligned::unique_ptr
and aligned::make_unique
(compiles with C++11, no need for C++14 support):
#ifndef ALIGNED_H
#define ALIGNED_H
#include <algorithm>
#include <memory>
// The "make_unique" parts are copied from GCC (/usr/include/c++/4.9/bits), and
// were adapted for alignment.
namespace aligned {
/// For internal use only!
namespace details {
/// Deleter for single object in aligned memory, used by aligned::unique_ptr
template <class T> struct Deleter {
void operator()(T *data) const {
// Single object, was created by placement-new => destruct explicitly
data->~T();
// Data allocated by "posix_memalign", so we must "free" it.
free(data);
}
};
/// Specialization of Deleter for array of objects, used by aligned::unique_ptr
template <class T> struct Deleter<T[]> {
void operator()(T *data) const {
// Data allocated by "posix_memalign", so we must "free" it.
free(data);
}
};
/// Allocation function for aligned memory, used by aligned::make_unique
template <typename T, std::size_t alignment>
inline typename std::remove_extent<T>::type *alloc(std::size_t num) {
// Ensure minimum alignment for given type
std::size_t align = std::max(std::alignment_of<T>::value, alignment);
// If T is an array type, we remove the "[]"
using TYPE = typename std::remove_extent<T>::type;
TYPE *mem = 0;
int error = posix_memalign((void **)&mem, align, sizeof(TYPE) * num);
if (error == EINVAL)
throw std::logic_error("Error: Alignment must be a power of two "
"(posix_memalign returned EINVAL)");
else if (error != 0)
throw std::bad_alloc();
return mem;
}
/// Default alignment is set to 64 Byte, i.e., the most common cache-line size.
/// This alignment is sufficient at the least for AVX-512.
constexpr std::size_t default_alignment = 64;
} // namespace details
/// Typedef providing aligned::unique_ptr
template <class T> using unique_ptr = std::unique_ptr<T, details::Deleter<T>>;
/// For internal use only!
namespace details {
template <typename T> struct MakeUniq { typedef unique_ptr<T> single_object; };
template <typename T> struct MakeUniq<T[]> { typedef unique_ptr<T[]> array; };
template <typename T, std::size_t Bound> struct MakeUniq<T[Bound]> {
struct invalid_type {};
};
} // namespace details
/// aligned::make_unique for single objects
template <typename T, std::size_t alignment = details::default_alignment,
typename... Args>
inline typename details::MakeUniq<T>::single_object
make_unique(Args &&... args) {
// Placement-new into aligned memory
// We use constructor with "{}" to prevent narrowing
return unique_ptr<T>(new (details::alloc<T, alignment>(1))
T{std::forward<Args>(args)...});
}
/// aligned::make_unique for arrays of unknown bound
template <typename T, std::size_t alignment = details::default_alignment>
inline typename details::MakeUniq<T>::array make_unique(std::size_t num) {
// We are not using "new", which would prevent allocation of
// non-default-constructible types, so we need to verify explicitly
static_assert(std::is_default_constructible<
typename std::remove_extent<T>::type>::value,
"Error: aligned::make_unique<T[]> supports only "
"default-constructible types");
static_assert(std::is_pod<
typename std::remove_extent<T>::type>::value,
"Error: aligned::make_unique<T[]> supports only "
"pod types");
return unique_ptr<T>(details::alloc<T, alignment>(num));
}
/// Disable aligned::make_unique for arrays of known bound
template <typename T, typename... Args>
inline typename details::MakeUniq<T>::invalid_type
make_unique(Args &&...) = delete;
} // namespace aligned
#endif // ALIGNED_H
Based on that we can allocate aligned memory (in this example I use the default alignment of 64 bytes instead of specifying it explicitly as a second template argument):
#include "aligned.h"
struct Foo {
Foo(int x, int y) : x(x), y(y){};
int x;
int y;
};
int main() {
// Single object
auto x = aligned::make_unique<double>(16.0);
// Forbidden thanks to "{}" --- did the user want to write
// aligned::make_unique<double[]>(16)?
// auto x = aligned::make_unique<double>(16);
auto foo = aligned::make_unique<Foo>(3, 4);
// Array
auto y = aligned::make_unique<double[]>(16);
// Disabled for arrays of known bounds:
// auto y = aligned::make_unique<double[16]>(16);
// Forbidden --- there is no default constructor:
// auto foo = aligned::make_unique<Foo[]>(16);
// Forbidden --- calling constructor & destructors on each array element is
// not implemented:
// auto s = aligned::make_unique<std::string[]>(16);
}
Is there any flaw or problem in my solution?
As @T.C. pointed out in a comment on Stack Overflow (where I had previously posted this question) there is a problem when allocating an array of, e.g., std::string
, because constructors and destructors must be called in that case. Therefore I currently disabled make_unique<T[]>
for non-POD types, but I would also appreciate a generic solution for that.
new
does not align memory to a sufficient extent when you want to use SSE, AVX, or other vector-instruction sets. For example, for SSE you need 16 byte alignment, for AVX 32 byte alignment.new
would align afloat
to 4 byte or adouble
to 8 byte, but that is not enough. I will edit the question to mention this. \$\endgroup\$posix_memalign
is not portable,std::align
+placement new
should be enough to make your code portable. \$\endgroup\$