6

I am following Stefanus Du Toit's hourglass pattern, that is, implementing a C API in C++ and then wrapping it in C++ again. This is very similar to the pimpl idiom, and it is also transparent to the user, but prevents more ABI-related issues and allows for a wider range of foreign language bindings.

As in the pointer-to-implementation approach, the underlying object's size and layout is not known by the outsiders at compile-time, so the memory in which it resides has to be dynamically allocated (mostly). However, unlike in the pimpl case, in which the object has been fully defined at the allocation point, here its properties are completely hidden behind an opaque pointer.

Memory obtained with std::malloc is "suitably aligned for any scalar type", which makes it unsuitable for the task. I'm not sure about the new-expression. Quoted from the section Allocation of the linked page:

In addition, if the new-expression is used to allocate an array of char or an array unsigned char, it may request additional memory from the allocation function if necessary to guarantee correct alignment of objects of all types no larger than the requested array size, if one is later placed into the allocated array.

Does this mean that the following code is compliant?

C API

size_t object_size      ( void );     // return sizeof(internal_object);
size_t object_alignment ( void );     // return alignof(internal_object);
void   object_construct ( void * p ); // new (p) internal_object();
void   object_destruct  ( void * p ); // static_cast<internal_object *>(p)->~internal_object();

C++ wrapper

/* The memory block that p points to is correctly aligned
   for objects of all types no larger than object_size() */
auto p = new char[ object_size() ];
object_construct( p );
object_destruct( p );
delete[] p;

If it is not, how to dynamically allocate properly-aligned memory?

Community
  • 1
  • 1
Kalrish
  • 1,938
  • 2
  • 16
  • 37
  • Note that [non-trivial and non-standard-layout objects are not guaranteed to occupy contiguous storage](http://stackoverflow.com/a/26228269/501250), so in those cases this technique would, I believe, be UB. – cdhowie Oct 20 '14 at 19:29
  • Nothing is being copied here, @Cdhowie. Alignment issues aside, I don't see why this technique shouldn't work. It's merely using placement new. Surely placement new works with non-trivial, non-standard-layout objects, no? (I only mean for single objects; [placement new for *arrays* is known to be broken](http://stackoverflow.com/q/8720425/33732) for a separate reason.) – Rob Kennedy Oct 20 '14 at 20:34
  • @RobKennedy I'm not really sure. If storage is not guaranteed to be contiguous then it does seem like a miracle that placement new works at all. I can't reconcile the two, TBH. – cdhowie Oct 20 '14 at 20:54

1 Answers1

1

I can't find where the standard guarantees your proposed code to work. First, I cannot find the part of the standard that backs what you've quoted from CppReference.com, but even if we take that claim on faith, it still only says that it may allocate additional space. If it doesn't, you're sunk.

The standard does speak to the alignment of memory returned by operator new[]: "The pointer returned shall be suitably aligned so that it can be converted to a pointer of any complete object type …." (C++03, §3.7.2.1/2; C++11, §3.7.4.1/2) However, in the context where you're planning to allocate the memory, the type you plan to store in it isn't a complete type. And besides, the result of operator new[] isn't necessarily the same as the result of the new-expression new char[…]; the latter is allowed to allocate additional space for its own array bookkeeping.

You could use C++11's std::align. To guarantee that you allocate space that can be aligned to the required amount, you'd have to allocate object_size() + object_alignment() - 1 bytes, but in practice, allocating only object_size() bytes will probably be fine. Thus, you might try using std::align something like this:

size_t buffer_size = object_size();
void* p = operator new(buffer_size);
void* original_p = p;
if (!std::align(object_alignment(), object_size(), p, buffer_size) {
  // Allocating the minimum wasn't enough. Allocate more and try again.
  operator delete(p);
  buffer_size = object_size() + object_alignment() - 1;
  p = operator new(buffer_size);
  original_p = p;
  if (!std::align(object_alignment(), object_size(), p, buffer_size)) {
    // still failed. invoke error-handler
    operator delete(p);
  }
}
object_construct(p);
object_destruct(p);
operator delete(original_p);

The allocator described in another question accomplishes much the same thing. It's templated on the type of object being allocated, which you don't have access to, but it's not required to be that way. The only times it uses its template type argument are to evaluate sizeof and alignof, which you already have from your object_size and object_alignment functions.

That seems like a lot to require from consumers of your library. It would be much more convenient for them if you moved the allocation behind the API as well:

void* object_construct() {
  return new internal_object();
}

Make sure to move the destruction, too, by calling delete, not just the destructor.

That makes any alignment questions go away because the only module that really needs to know it is the one that already knows everything else about the type being allocated.

Community
  • 1
  • 1
Rob Kennedy
  • 156,531
  • 20
  • 258
  • 446
  • That was what I was doing. I chose to move memory management out of the API because it allowed to have static storage (see [this question](http://stackoverflow.com/questions/4921932/pimpl-idiom-without-using-dynamic-memory-allocation)). However, I still want to provide a fallback if the estimated size or alignment is not suitable, i.e., use `std::aligned_storage` if `EstimatedSize >= object_size() && EstimatedAlignment >= object_alignment()`. – Kalrish Oct 20 '14 at 17:56