Как использовать new со структурой переменного размера?

В Си иногда использовался такой подход: tio.run

#include <stdlib.h>
#include <stdio.h>

struct smth {
  int size;
  int data[0];
};

int main()
{
  smth *x = (smth*)malloc(sizeof (smth) + 3 * sizeof (int));
  x->size = 3;
  x->data[0] = 1;
  x->data[1] = 2;
  x->data[2] = 3;

  for (int q=0; q<x->size; ++q)
    printf("%d ", x->data[q]);

  free(x);
}

Есть ли у него аналог в Си++? Как выделить память нужного размера через new?


Ответы (4 шт):

Автор решения: ueber

Подобному юзкейсу посвящено одно из изменений в 20 стандарт - https://wg21.link/p0722. В нём приводится пример класса переменного размера:

class inlined_fixed_string {
  public:
   inlined_fixed_string() = delete;
   const size_t size() const { return size_; }

   const char *data() const {
     return static_cast<const char *>(this + 1);
   }

   // operator[], etc, with obvious implementations

   inlined_fixed_string *Make(const std::string &data) {
     size_t full_size = sizeof(inlined_fixed_string) + data.size();
     return new(::operator new(full_size))
                  inlined_fixed_string(data.size(), data.c_str());
   }

  private:
   inlined_fixed_string(size_t size, const char *data) : size_(size) {
     memcpy(data(), data, size);
   }
   size_t size_;
};
→ Ссылка
Автор решения: AR Hovsepyan

Если я правильно понимаю, то речь идет о том, как поместить экземпляр структуры в память переменного размера оператором new. Иначе говоря, это new с размещением. Примерно сделаю то, что показано в вопросе по возможности примитивно, чтобы было понятней:

 struct smth {
    char i{ 'a' };   
};

int main()
{    
    char* s1 = new char[5]; 
    // разместить  smth в s1
    smth* ps = new(s1)(smth);
    //инициализация массива через smth
    for(size_t i = 0; i < 4; ++i)
         (ps + i)->i = 'a' + i; //abcde
    //ставим символ конца, чтобы использовать
    s1[4] = '\0';
    //и выводим результат
    std::cout << '\n' << s1; //abcd
    delete[]s1;
    //теперь берем другой участок памяти
    s1 = new char[16];
    //разместить  smth в s1
    smth* ps1 = new(s1)(smth);
    for (size_t i = 0; i < 15; ++i)
        (ps1 + i)->i = 'z' - i; //abcde
    s1[14] = '\0';
    std::cout << '\n' << s1;
    //zyxwvutsrqponm
    
    delete[]s1;
    return 0;
}
→ Ссылка
Автор решения: AlexGlebe

В плюсах такого нет. Приходится использовать ручное выделение памяти и вызовы конструктов и деструкторов. Переделываем пример на классы, для демонстрации работы.

# include <iostream>

class A {
public:
  int i{};
  A(){std::cout<<"A() "<<std::flush;}
  ~A(){std::cout<<"~A() "<<std::flush;}
};

class C {
public  :
  size_t  size  { 0 } ;
  inline  A * Data  ( ) { return  ( A * ) ( this + 1 ) ; }
  C ( size_t ) ;
  ~ C ( ) ;
} ;

C :: C ( size_t const s ) : size { s } {
  std::cout<<"C() "<<std::flush;
  new ( Data  ( ) ) A [ s ] ;
}

C :: ~C ( ) {
  A * a = Data  ( ) ;
  for ( size_t  i = 0 ; i < size ; ++ i , ++ a )
    a -> ~ A ( ) ;
  std::cout<<"~C() "<<std::endl;
}

int main  ( ) {
  int const s = 3 ;
  C * const cp  = ( C * ) :: operator new
    ( sizeof ( C ) + sizeof ( A [ s ] ) ) ;
  new ( cp  ) C ( s ) ;
  cp -> ~ C ( ) ;
  :: operator delete ( cp ) ;
}

проверяем

$ ./vararray 
C() A() A() A() ~A() ~A() ~A() ~C() 

То есть выделяем голую память под класс C плюс массив.

Конструктор вызываем вручную.

В конструкторе класса C делаем ручные вызовы конструкторов в дополнительном массиве.

Деструктор класса вызываем вручную.

При вызове деструктора C он будет сам вызывать вручную деструкторы вектора и дальше сам очищается.

Отдаём выделенную голую память.

→ Ссылка
Автор решения: HolyBlackCat

Начнем с того, что ваш код - невалидный C/C++. Его можно сделать валидным C, заменив [0] на [] и исправив smth на struct smth.

Такой массив без размера в конце структуры называется flexible array member, их нет в C++.


Но можно самостоятельно соорудить альтернативу.

Для этого убираете массив из класса: struct smth {int size;};, и дальше вычисляете адрес "массива" относительно this, что-то типа reinterpret_cast<char *>(this) + sizeof(*this). Потом выровнять указатель (в вашем случае нет необходимости), и для надежности можно "отмыть" результат через std::launder.

У меня нет полной уверенности, что такие манипуляции с адресами легальны (даже с std::launder), но я не верю, что такой популярный прием сломается на практике, поэтому неважно.

Проблема: хочется мочь писать delete x;, вместо того чтобы руками делать x->~MyClass(); operator delete(x); (где operator delete() - это плюсовая замена free(&x)).

Многоуважаемый @ueber откопал ссылку на P0722R1 (который попал в C++20, см. конкретные изменения в стаднарте в P0722R3), который объясняет, что просто так писать delete x; для таких классов нельзя - ведь теперь в operator delete() вторым параметром автоматически передается размер куска памяти, т.е. sizeof(T). (Пишут, что это дает возможность делать более эффективные аллокаторы.) Т.к. в вашем случае sizeof(T) и размер блока памяти не совпадают, вы получаете UB.

P0722R1 объясняет решение: внутрь класса нужно добавить

void operator delete(void *ptr) {::operator delete(ptr);}

Это блокирует передачу размера класса в operator delete, и убирает UB. Но P0722R1 также объясняет, что "безразмерная" версия operator delete может работать медленнее, чем версия с размером.

Можно было бы самостоятельно передать размер блока памяти в operator delete, вычислив его из поля size вашего класса, вот так: void operator delete(void *ptr) {::operator delete(ptr, размер);}. Но его невозможно нормально сделать, потому что деструктор вашего класса уже отработал, и попытка прочитать size вызовет неопределенное поведение, даже если вы не меняли его значение в деструкторе.

Поэтому в C++20 (этим P0722R3) был добавлен т.н. destroying operator delete ("разрушающий" вариант delete).

Он используется следующим образом:

struct A
{
    int size = 0;

    void operator delete(A *a, std::destroying_delete_t)
    {
        std::size_t mem_size = sizeof(A) + a->size * sizeof(int);
        a->~A();
        ::operator delete((void*)a, mem_size);
    };
};

Добавляя параметр std::destroying_delete_t вы отказываетесь от автоматического вызова деструктора, и должны сделать это сами (поэтому "разрушающий"). Это позволяет вам прочитать поля класса до вызова деструктора, и вычислить из них размер освобождаемой памяти. Также тип первого параметра меняется с void * на MyClass *. (Что интересно, в Clang-е delete с размером во втором параметре компилируется только с доп. флагом - -fsized-deallocation.)


Остается только написать красивую функцию для выделения нужного объема памяти и создания в ней объекта. Что-то в духе:

void *ptr = ::operator new(sizeof(A) + size * sizeof(int));
return new(ptr) A(size);

В той же самой P0722R1 есть интересная идея: делать это вычисление в кастомном operator new внутри класса:

struct A
{
    int size = 0;

    A(int size) : size(size) {}

    void *operator new(std::size_t n, int x)
    {
        return ::operator new(n + x * sizeof(int));
    }

    // ... operator delete ...
};

И вызывается эта бандура так: new(n) A(n); (где первый n передается в new, а второй в конструктор). В чем профит такого подхода - непонятно. Все равно надо заворачивать в функцию, чтобы два раза не писать размер, и чтобы возвращать сразу умный указатель.


Если хотите делать такой класс шаблоном, для произвольных типов, еще нужно особым образом обрабатывать типы с большим выравниванием (больше __STDCPP_DEFAULT_NEW_ALIGNMENT__). Есть специальные перегрузки operator new/operator delete, принимающие выравнивание - про них надо гуглить отдельно.

→ Ссылка