14
\$\begingroup\$

I wrote a short class which allocates objects on the stack and aligns them to a specific memory address. Alignment makes sense for some objects (i.e. some SIMD intrinsics require aligned data). I wrote this mostly as an exercise. Tests are included. If someone has time to skim over this: thanks in advance, I'm looking forward to learn from your comments!

Also I'd be curious if you would add more functionality, i.e. operator== etc.

Usage

  // Allocates an int object aligned to 8 bytes, initializes it with 4
  aligned<8, int> i8(4);
  // Get a pointer to the actual value with operator&
  int *pi8 = &i8;
  // Get the actual value with operator*
  assert(*i8 == 4);

Header file

#include <cstdint>

template<size_t Align, typename T>
class aligned
{
  public:
    using value_type = T;
    using aligned_type = aligned<Align, T>;

    constexpr aligned() : ptr_(nullptr) {
    }

    aligned(const aligned_type &other) {
      *ptr_ = *other.ptr_;
    }

    aligned(const aligned_type &&other) {
      *ptr_ = std::move(*other.ptr_);
    }

    aligned_type &operator=(const aligned_type &other) {
      *ptr_ = *other.ptr_;
      return *this;
    }

    template<typename... Args>
    aligned(Args&&... args) {
      ptr_ = aligned_position();
      new (ptr_) T(std::forward<Args>(args)...);
    }

    ~aligned() {
      ptr_->~T();
    }

    T *get() const {
      return ptr_;
    }

    T *operator&() const {
      return get();
    }

    T operator*() const {
      return *ptr_;
    }

  private:
    T *aligned_position() {
      uint8_t *p = &storage_[0];
      while (reinterpret_cast<unsigned long>(p) % Align != 0)
        p++;
      return reinterpret_cast<T *>(p);
    }

    uint8_t storage_[Align - 1 + sizeof(T)];
    T *ptr_;
};

Tests

The main function has a few the tests. You can use operator& to get a pointer to the (aligned) object, or operator* to get a reference.

#include <cassert>
#include <utility>

int
main(int argc, const char *argv[])
{
  aligned<16, char> a;
  assert(&a == nullptr);

  aligned<8, int> i8(4);
  int *pi8 = &i8;
  assert(reinterpret_cast<unsigned long>(pi8) % 8 == 0);
  assert(*i8 == 4);

  aligned<7, double> d7(12.123);
  double *pd7 = &d7;
  assert(reinterpret_cast<unsigned long>(pd7) % 7 == 0);
  assert(*d7 == 12.123);

  using P = std::pair<int, int>;
  aligned<13, P> p13({1, 2}); // testing a more complex object
  P *pp13 = &p13;
  assert(reinterpret_cast<unsigned long>(pp13) % 13 == 0);
  assert(*p13 == std::make_pair(1, 2));
  return 0;
}

I use the GNU g++ compiler, version 5.4.0.

\$\endgroup\$
  • 3
    \$\begingroup\$ Is there a reason to use this rather than standard C++ alignas? \$\endgroup\$ – Toby Speight Jun 1 '17 at 12:41
  • 3
    \$\begingroup\$ Didn't know alignas, thanks for pointing it out. But as i said, this was just an exercise. \$\endgroup\$ – cruppstahl Jun 1 '17 at 12:44
  • 2
    \$\begingroup\$ That's fine; it's perfectly good for review. I just wasn't sure whether you were solving a different problem or not! \$\endgroup\$ – Toby Speight Jun 1 '17 at 12:45
18
\$\begingroup\$

Silent bug: Bad move constructor

Your move constructor will perform a copy because your parameter is aligned_type const&&; you cannot move from a const value because moving implies modifying what you're moving from. Simply change the signature to:

aligned(aligned_type &&other);
//      ^^^^^^^^^^^^^^^ no more const

Bug: Bad dereference operator

Your dereference operator is defined like so:

T operator*() const {
// we don't really want to copy the value out every single time we deref, do we?
  return *ptr_;
}

It simply returns T, it should return T&. Note that this is a source of a bug in your code. You end up creating a temporary when you assign by using the dereference operator. This can (and did on my system) result on a crash due to the memory access violation.

Only align to alignments that are powers of 2.

Your test code aligns to 7 and 13.

From [basic.align] in the C++ Standard (emphasis mine):

Alignments are represented as values of the type std​::​size_­t. Valid alignments include only those values returned by an alignof expression for the fundamental types plus an additional implementation-defined set of values, which may be empty. Every alignment value shall be a non-negative integral power of two.

T* aligned_position();

This function is used in only one place. Unless you've got plans of using it elsewhere, I recommend erasing it and simply putting the code at the relevant location.

You don't have to index into the first element to get the address. You can use the fact that arrays decay to pointers.

uint8_t *p = storage_; // equivalent to uint8_t *p = &storage_[0];

However, there is a faster way than looping in order to align an address to some boundary:

auto aligned_position() const noexcept
{
    return reinterpret_cast<T*>(
        (reinterpret_cast<std::uintptr_t>(&storage_) + Align - 1) & ~(Align - 1));
}

Do we really need aligned_position(); and ptr_?

No. Here is an alternative solution:

You currently allocate Align - 1 + sizeof(T) bytes. This is wasteful. You can use exactly as much space as you need by declaring your storage any of these two ways:

  1. alignas(Align) std::uint8_t storage_[sizeof(T)];
  2. std::aligned_storage_t<sizeof(T), Align> storage_; // from the <type_traits> header

Option 1 might be preferable because option 2 is not guaranteed to align to your requested boundary if Align > alignof(std::max_align_t). In such a case, it will depend on the implementation of the standard library you're using; the Microsoft implementation does this.

Before: sizeof(aligned<8, double>) == 20

After: sizeof(aligned<8, double>) == 16

Wait, we're not done yet. We shouldn't align to the specified alignment blindly either because we can then specify an alignment of 4 for a double, which requires 8. Unaligned data like that becomes very slow to access (two reads instead of one) or can simply not work (crash) in certain systems.

The fix:

alignas(Align < alignof(T) ? alignof(T) : Align) std::uint8_t storage_[sizeof(T)];

Note that we no longer need ptr_ because the first byte of storage_ now corresponds to a properly aligned byte for our type.

Before: sizeof(aligned<8, double>) == 20

After: sizeof(aligned<8, double>) == 8

We save quite a bit of space. In fact, with this solution, your class takes no more space than a T when aligned to alignof(T). You have to replace all uses of ptr_ with an appropriate statement; I recommend replacing all instances of ptr_ with a call to get(). This call will almost surely be optimized away.

Provide non-const versions of your accessor/modifier functions.

You want your type to be usable in non-const contexts, so provide non-const overloads for your operations. For example, define get() like so:

T* get() noexcept {
    return reinterpret_cast<T*>(&storage_);
}

T const* get() const noexcept {
    return reinterpret_cast<T const*>(&storage_);
}

You can provide this functionality wherever needed.


Final thoughts:

  • The class is basically a wrapper around a aligned_storage<> like storage. However, it is a good learning experience I suppose.
  • I've left out basic language features advice such as declaring functions constexpr or giving them exceptions specifications through type traits. Please look into that yourself.
\$\endgroup\$

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.