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.
2 Answers 2
Looks reasonable to me. Good use of standard components to build the thing you want!
TYPE *mem = 0;
int error = posix_memalign((void **)&mem, align, sizeof(TYPE) * num);
I'd have written
void *mem = nullptr;
int error = posix_memalign(&mem, align, sizeof(TYPE) * num);
to get rid of the ugly casts and 0
-as-null. (Then return static_cast<TYPE*>(mem)
.)
template <class T> using unique_ptr = std::unique_ptr<T, details::Deleter<T>>;
I have to keep in mind while reading your code that unique_ptr<T>
and std::unique_ptr<T>
are different types (despite being spelled the same). That's a good design choice for the user of your library, but it's a terrible design choice for the reader of your library. Therefore, IMO you should create a new name for your type in this file, e.g. details::uniq_ptr
; and then at the very end of the file you should introduce template<class T> using unique_ptr = details::uniq_ptr<T>;
. That way you get the best of both worlds: the user gets two things with the same name, and the library reader gets distinct names for distinct concepts.
For make_unique<T[]>
, you'll have to return an aligned::unique_ptr<T[]>
, which must remember the size of the array (or else retrieve it from something like malloc_usable_size
or _msize
, but those are problematic because they're analogous to vector::capacity
instead of vector::size
. You don't want to destroy more items than the user provided). Fortunately you can partially specialize your unique_ptr
template so that unique_ptr<T[]>
has that extra "size" member. (The STL's unique_ptr
is also specialized for T[]
, but doesn't keep that explicit "size" member because operator delete[]
doesn't need it. Instead, it's specialized to provide a different set of accessor operators: []
instead of *
and so on.)
// We use constructor with "{}" to prevent narrowing
// Forbidden thanks to "{}" --- did the user want to write
// aligned::make_unique<double[]>(16)?
// auto x = aligned::make_unique<double>(16);
This is a breaking change / deliberate asymmetry compared to std::unique_ptr<T>
. What's wrong with narrowing? Your example implies that you're worried someone might accidentally leave the []
off an array type, but that's pretty far-fetched, isn't it?
-
\$\begingroup\$ I agree with your comments 1,2, and 4. I do not quite understand what you mean in your 3rd comment though.
posix_memalign
stores the size somehow, sofree
will do its job properly. \$\endgroup\$Simon– Simon2015年05月14日 19:49:56 +00:00Commented May 14, 2015 at 19:49 -
\$\begingroup\$ Regarding the suggestion in your 2nd comment: You give
details::uniq_ptr
as an example. Do you see any reason why I should not just spell out the type including the namespace name, i.e., writealigned::unique_ptr
everywhere in the header? \$\endgroup\$Simon– Simon2015年05月14日 20:01:17 +00:00Commented May 14, 2015 at 20:01 -
\$\begingroup\$ @simon re 3rd comment: You asked how
unique_ptr<T[]>
for non-PODT
should figure out how many objects to destroy. The answer is, you'll have to keep track of that in a member variable or else rely on_msize
to guess how many you have. @simon re 2nd comment: Sure, I'd be reasonably happy with consistent use ofstd::unique_ptr
andaligned::unique_ptr
(and no use of unqualifiedunique_ptr
... except that some uses (such as in constructor declarations) have to be unqualified; that might be a bit confusing. It's still strictly better than your original all-unqualified approach, though! \$\endgroup\$Quuxplusone– Quuxplusone2015年05月15日 18:05:40 +00:00Commented May 15, 2015 at 18:05
I saw this question on stackoverflow and it got me thinking there. I started to wonder if you could do what you wanted in a portable way using only the c++11 standard, while leveraging existing types to guarantee exception safety, RAII etc.
This is what I came up with:
template<class T, size_t Alignment = 64>
struct aligned_ptr {
static constexpr size_t alignment = (Alignment < alignof(T)) ? alignof(T) : Alignment;
static constexpr size_t buffer_size = alignment + sizeof(T) - 1;
template<class...Args>
aligned_ptr(Args&&...args)
: _memory { new uint8_t[buffer_size] }
, _object { make_object_pointer(_memory.get(), std::forward<Args>(args)...), &deleter }
{
}
T* get() const noexcept {
return reinterpret_cast<T*>(_object.get());
}
private:
static void deleter(T* p) noexcept(noexcept(std::declval<T>().~T()))
{
p->~T();
// note: not freed
}
template<class...Args>
T* make_object_pointer(uint8_t* buffer, Args&&...args)
{
auto addr_v = reinterpret_cast<void*>(buffer);
auto space = buffer_size;
std::align(alignment, sizeof(T), addr_v, space);
auto address = reinterpret_cast<T*>(addr_v);
new (address) T (std::forward<Args>(args)...);
return address;
}
private:
// NOTE: order matters. memory before object
std::unique_ptr<uint8_t[]> _memory;
std::unique_ptr<T, void(*)(T*)> _object;
};
Some work could be done on allocation to reduce waste (pre-caching etc) but I think it's a solid footing:
-
\$\begingroup\$ Can you explain your code at all, how it differs from the existing code? The purpose of Codereview.se is not to write new code; it's to critique the existing code and explain how it can be improved. \$\endgroup\$Snowbody– Snowbody2015年05月14日 18:47:20 +00:00Commented May 14, 2015 at 18:47
-
\$\begingroup\$ This code provides examples of how the author can remove all non-portable code, for example the posix_ calls. \$\endgroup\$Richard Hodges– Richard Hodges2015年05月14日 19:07:16 +00:00Commented May 14, 2015 at 19:07
-
\$\begingroup\$ I am not sure I get your intention. What part of my solution would you replace by your code? The intention of my code is to explicitly provide an interface as provided by
std::unique_ptr
andstd::make_unique
, so if a user needs aligned memory, he can simply usealigned::unique_ptr
instead. \$\endgroup\$Simon– Simon2015年05月14日 19:53:43 +00:00Commented May 14, 2015 at 19:53 -
\$\begingroup\$ @Simon your code is not portable (
posix_memalign
). How would you write youraligned_ptr
class if you couldn't useposix_memalign
? The C++ standard providesstd::align
andplacement new
, so @Richard uses those to avoid usingposix_memalign
. As a consequence @Richard's suggestion works in non-POSIX environments. \$\endgroup\$gnzlbg– gnzlbg2015年09月21日 21:06:46 +00:00Commented Sep 21, 2015 at 21:06 -
\$\begingroup\$ @gnzlbg I used
posix_memalign
merely as an example. You can certainly replace it with something portable without impacting the rest of the design. @Richard's answer left things rather unclear (not providing functionality along the lines ofstd::unique_ptr
andstd::make_unique
, which was the original intention). \$\endgroup\$Simon– Simon2015年09月22日 19:18:34 +00:00Commented Sep 22, 2015 at 19:18
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\$