Как использовать 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 шт):
Подобному юзкейсу посвящено одно из изменений в 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_;
};
Если я правильно понимаю, то речь идет о том, как поместить экземпляр структуры в память переменного размера оператором 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;
}
В плюсах такого нет. Приходится использовать ручное выделение памяти и вызовы конструктов и деструкторов. Переделываем пример на классы, для демонстрации работы.
# 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 он будет сам вызывать вручную деструкторы вектора и дальше сам очищается.
Отдаём выделенную голую память.
Начнем с того, что ваш код - невалидный 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, принимающие выравнивание - про них надо гуглить отдельно.