9

I know I can do new char[n] to create an array of n chars. This works even when n is not a compile time constant.

But lets say I wanted a size variable followed by n chars:

My first attempt at this is the following:

struct Test
{
  std::size_t size;
  char a[];
};

However it seems new Test[n] doesn't do what I expect, and instead allocates n sizes.

I've also found that sizeof(std::string) is 4 at ideone, so it seems it can allocate both the size and the char array in one block.

Is there a way I can achieve what I described (presumably what std::string already does)?

Clinton
  • 22,361
  • 15
  • 67
  • 163

5 Answers5

13

While you can do this (and it was often used in C as a workaround of sorts) it's not recommended to do so. However, if that's really what you want to do... here's a way to do it with most compilers (including those that don't play nicely with C99 enhancements).

#define TEST_SIZE(x) (sizeof(Test) + (sizeof(char) * ((x) - 1)))

typedef struct tagTest
{
    size_t size;
    char a[1];
} Test;

int elements = 10; // or however many elements you want
Test *myTest = (Test *)malloc(TEST_SIZE(elements));

The C specifications prior to C99 do not allow a zero-length array within a structure. To work around this, an array with a single element is created, and one less than the requested element count is added to the size of the actual structure (the size and the first element) to create the intended size.

Adam Maras
  • 26,269
  • 6
  • 65
  • 91
  • I could see how this would work in this case, but wouldn't this be an issue if change `size_t` with `short`, and `char` to `long`, resulting in all the `long`s being not aligned correctly (or alternatively, not allocating enough space)? – Clinton Oct 03 '11 at 23:33
  • 2
    Unless you use compiler directives to manually set the packing of the structure, if you had a `short` as a first member and defined the array as `long`, the compiler would start the `short` at byte 0 (through byte 1), skip bytes 2 through 7, and then start the `long` at byte 8 (keeping it properly aligned). Then, each `long` allocated/accessed after that would be on an 8-byte alignment. – Adam Maras Oct 03 '11 at 23:42
  • This works because `sizeof(Test)` will resolve to 8 (per my above comment) because the size of the structure will include the empty bytes used for alignment. If you tried to allocate using `sizeof(short) + (sizeof(long) * x)`, you would inevitably allocate too little space. – Adam Maras Oct 03 '11 at 23:44
  • Is there anyway to do this with `new`, or is malloc the only option here? (of course I'll wrap all this in a constructor, so it isn't an issue using malloc, it's just out of interest). – Clinton Oct 04 '11 at 02:17
  • @Clinton: I think this could also be done using [placement new](http://stackoverflow.com/questions/222557/cs-placement-new). – Cameron Oct 04 '11 at 02:36
  • @Cameron placement `new` is only useful after a buffer has been allocated (usually with `malloc`). – Adam Maras Oct 04 '11 at 03:22
  • @Adam: Right, but you could allocate the initial buffer with `new`... although I suppose there'd be no point in using placement new after that, since you could just use the pointer to the region directly. I wasn't thinking clearly ;-) – Cameron Oct 04 '11 at 04:40
10

You can use placement new:

#include <new>

struct Test {
    size_t size;
    char a[1];

    static Test* create(size_t size)
    {
        char* buf = new char[sizeof(Test) + size - 1];
        return ::new (buf) Test(size); 
    }

    Test(size_t s) : size(s)
    {
    }

    void destroy()
    {
        delete[] (char*)this;
    }
};

int main(int argc, char* argv[]) {
    Test* t = Test::create(23);
    // do whatever you want with t
    t->destroy();
}
R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
Miguel Grinberg
  • 65,299
  • 14
  • 133
  • 152
  • Miguel: as you're allocating a char array with `new`, couldn't there be potential issues with alignment when you use it to store a `Test`? Is there anyway to force alignment on `new`? – Clinton Oct 04 '11 at 04:01
  • I don't think there will be any alignment problems. If the compiler inserts padding in the struct then sizeof() will report it, so the padding will be counted in the memory allocated with new. I think you could end up allocating a few extra bytes with this algorithm, but never less. – Miguel Grinberg Oct 04 '11 at 04:17
  • @Clinton: `new[]` allocates storage properly aligned for any type with size equal or less than the requested size, so that's not an issue here. However, accessing an array index greater than or equal to its size is undefined behaviour. Make of that what you will. – R. Martinho Fernandes Oct 04 '11 at 13:05
  • It would cause undefined behaviour if someone tried to access elements of `t->a` other than `t->a[0]` – M.M Nov 26 '18 at 03:00
7

You can also use the "Length 1 Array" trick. This is in C:

struct Test {
    size_t size;
    char a[1];
}

int want_len = 2039;
struct Test *test = malloc(sizeof(struct Test) + (want_len - 1));
test->size = want_len;

GCC also supports "0 length" arrays for exactly this purpose: http://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html

Yann Ramin
  • 32,895
  • 3
  • 59
  • 82
2

If I understand correctly, you want a class that stores a single pointer to a dynamically allocated length-prefixed string. You can do that by taking advantage of the fact that char* can safely alias anything.

A simplistic implementation, just to show how it can be done:

class LPS
{
private:
    char* ptr;

public:
    LPS() noexcept : ptr(nullptr) {} // empty string without allocation
    explicit LPS(std::size_t len) {
        // Allocate everything in one go
        // new[] gives storage aligned for objects of the requested size or less
        ptr = new char[sizeof(std::size_t) + len];
        // Alias as size_t
        // This is fine because size_t and char have standard layout
        *reinterpret_cast<std::size_t*>(ptr) = len;
    }
    explicit LPS(char const* sz) {
        std::size_t len = std::char_traits<char>::length(sz);
        ptr = new char[sizeof(std::size_t) + len;
        *reinterpret_cast<std::size_t*>(ptr) = len;
        std::copy(sz, sz + len, ptr + sizeof(std::size_t));
    }
    LPS(LPS const& that) {
        if(that.ptr) {
            ptr = new char[sizeof(std::size_t) + that.size()];
            std::copy(that.ptr, that.ptr + sizeof(std::size_t) + that.size(), ptr);
        } else ptr = nullptr;
    }
    LPS(LPS&& that) noexcept {
        ptr = that.ptr;
        that.ptr = nullptr;
    }
    LPS& operator=(LPS that) {
        swap(that);
        return *this;
    }
    ~LPS() noexcept {
        // deleting a null pointer is harmless, no need to check
        delete ptr;
    }
    void swap(LPS& that) noexcept {
        std::swap(ptr, that.ptr);
    }
    std::size_t size() const noexcept {
         if(!ptr) return 0;
         return *reinterpret_cast<std::size_t const*>(ptr);
    }
    char* string() noexcept {
         if(!ptr) return nullptr;
         // the real string starts after the size prefix
         return ptr + sizeof(std::size_t);
    }
};
R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • This doesn't address OP's concern about wanting to allocate the entire object in one heap area (as opposed to allocating an object on the heap that carries a pointer to another object on the heap, like most string classes.) – Adam Maras Oct 03 '11 at 23:59
  • @AdamMaras I think it does. The question reads "(...) it seems it can allocate both the size and the char array in one block.", which is exactly what this does. I'll let the OP decide. – R. Martinho Fernandes Oct 04 '11 at 00:13
  • 1
    @AdamMaras Don't allocate the object dynamically and suddenly there's only one layer of indirection. Like most string classes: who does `std::unique_ptr(new std::string)`? – Luc Danton Oct 04 '11 at 03:56
-1

Let's keep things short and sweet in C++ using std::vector.

struct Test
{
   std::size_t size;
   char *a;  // Modified to pointer

   Test( int size ): size(size), a(new char[size+1])
   {}
};

std::vector<Test> objects(numberOfObjectsRequired,argumentToTheConstructor);
Mahesh
  • 34,573
  • 20
  • 89
  • 115