MvG
MvG

Reputation: 60918

Variable-size object in shared_ptr

Suppose I want a variable-size array with a bit of a header up front stored in a std::shared_ptr. I could do something like

#include <memory>
using namespace std;
struct obj {
  char headerCode;
  unique_ptr<short[]> data;
};
shared_ptr<obj> make(unsigned len) {
  return shared_ptr<obj>{new obj{'x', unique_ptr<short[]>{new short[len]}}};
}

But this would lead to three allocations: one for the shared_ptr control block, one for the obj and one for its data. With make_shared it is possible that the first two share some memory:

#include <memory>
using namespace std;
struct obj {
  char headerCode;
  unique_ptr<short[]> data;
  obj(char headerCode, short data[]) : headerCode(headerCode), data(data) {}
};
shared_ptr<obj> make(unsigned len) {
  return make_shared<obj>('x', new short[len]);
}

With low-level allocation I can make the object and its data share some memory:

#include <memory>
#include <cstdlib>
using namespace std;
struct obj {
  char headerCode;
  short data[0];
};
shared_ptr<obj> make(unsigned len) {
  obj* o = reinterpret_cast<obj*>(malloc(sizeof(obj) + len*sizeof(short)));
  o->headerCode = 'x';
  return shared_ptr<obj>(o, free);
}

Are these two techniques allowed by the standard? If not, is there something similar which is allowed? Is there something which makes this work in a standards-conforming way with only a single memory allocation? Preferrably without having to store an allocator or deleter object in every instance?

Upvotes: 1

Views: 1939

Answers (1)

Jonathan Wakely
Jonathan Wakely

Reputation: 171343

Are these two techniques allowed by the standard?

The first one is fine, the second uses a zero-length array as a member (data[0]) which is not valid C++, but is supported as an extension by some compilers.

Is there something which makes this work in a standards-conforming way with only a single memory allocation?

Currently it's pretty difficult without http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3641.html which would allow something like this:

struct obj {
  char headerCode;
  short* data;
};

shared_ptr<obj> make(unsigned len) {
  auto p = make_shared<char[]>(sizeof(obj) + sizeof(short)*len));
  short* s = static_cast<short*>(p.get() + sizeof(obj));
  return shared_ptr<obj>(p, ::new(p.get()) obj{'x', s});
}

This allocates an array of char with enough space for an obj and the array of short, then uses placement new to construct an obj into that memory, and creates another shared_ptr which shares ownership with the one that owns the char array. When the last shared_ptr that owns the memory drops its reference the char array will be correctly deleted, but the obj object's destructor will not be run, so it's important that you don't rely on its destructor having any side effects (ideally it will have a trivial destructor).

You might be able to do it today using allocate_shared with a custom allocator which allocates additional space for the array of short, but since you don't know how the implementation will arrange the control block and your object in the space allocated it's quite difficult to work out where the short array begins, and the allocator needs to be stored in the control block.

Upvotes: 1

Related Questions