4
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ You still have to call the destructor for arrays. \$\endgroup\$ – Martin York May 14 '15 at 21:11
  • \$\begingroup\$ @LokiAstari Could you please elaborate, I don't understand your comment. \$\endgroup\$ – Simon May 15 '15 at 9:04
  • \$\begingroup\$ 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 a float to 4 byte or a double to 8 byte, but that is not enough. I will edit the question to mention this. \$\endgroup\$ – Simon May 15 '15 at 9:37
  • \$\begingroup\$ posix_memalign is not portable, std::align + placement new should be enough to make your code portable. \$\endgroup\$ – gnzlbg Sep 21 '15 at 21:10
2
\$\begingroup\$

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?

\$\endgroup\$
  • \$\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, so free will do its job properly. \$\endgroup\$ – Simon May 14 '15 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., write aligned::unique_ptr everywhere in the header? \$\endgroup\$ – Simon May 14 '15 at 20:01
  • \$\begingroup\$ @simon re 3rd comment: You asked how unique_ptr<T[]> for non-POD T 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 of std::unique_ptr and aligned::unique_ptr (and no use of unqualified unique_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 May 15 '15 at 18:05
1
\$\begingroup\$

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:

\$\endgroup\$
  • \$\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 May 14 '15 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 May 14 '15 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 and std::make_unique, so if a user needs aligned memory, he can simply use aligned::unique_ptr instead. \$\endgroup\$ – Simon May 14 '15 at 19:53
  • \$\begingroup\$ @Simon your code is not portable (posix_memalign). How would you write your aligned_ptr class if you couldn't use posix_memalign? The C++ standard provides std::align and placement new, so @Richard uses those to avoid using posix_memalign. As a consequence @Richard's suggestion works in non-POSIX environments. \$\endgroup\$ – gnzlbg Sep 21 '15 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 of std::unique_ptr and std::make_unique, which was the original intention). \$\endgroup\$ – Simon Sep 22 '15 at 19:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.