массивы — Структурный эквивалент взлома в переполнении стека

Хак с структурой, где у вас есть массив длины 0 в качестве последнего члена структуры из C90 и C99, хорошо известен, и с введением гибких членов массива в C99 мы даже получили стандартизированный способ его использования с [], К сожалению, C ++ не предоставляет такой конструкции, и (по крайней мере, с Clang 3,4), компилируя структуру с [0] или же [] выдаст предупреждение компиляции с --std=c++11 -pedantic:

$ cat test.cpp
struct hack {
char filler;
int things[0];
};
$ clang++ --std=c++11 -pedantic test.cpp
\test.cpp:3:14: warning: zero size arrays are an extension [-Wzero-length-array]
int things[0];

и так же

$ cat test.cpp
struct fam {
char filler;
int things[];
};
$ clang++ --std=c++11 -pedantic test.cpp
\test.cpp:3:7: warning: flexible array members are a C99 feature [-Wc99-extensions]
int things[];

Тогда мой вопрос таков: скажем, что я хочу иметь структуру, которая содержит массив переменного размера в качестве последнего элемента в C ++. Как правильно поступить с компилятором, который поддерживает оба? Должен ли я пойти с взломать структуру [0] (который является расширением компилятора), или FAM [] (что является функцией C99)? Насколько я понимаю, либо сработает, но я пытаюсь выяснить, что является меньшим злом?

Кроме того, прежде чем люди начнут предлагать сохранить int* вместо этого выделенному фрагменту памяти в структуре, это не является удовлетворительным ответом. Я хочу выделить один фрагмент памяти для хранения моей структуры и элементов массива. Использование std :: vector также относится к той же категории. Если вам интересно, почему я не хочу использовать указатель вместо этого, Ответ Р. на другой вопрос дает хороший обзор.

В других местах были подобные вопросы, но ни один из них не дал ответа на этот конкретный вопрос:

22

Решение

Вы можете получить более или менее тот же эффект, используя член
функция и reinterpret_cast:

int* buffer() { return reinterpret_cast<int*>(this + 1); }

Это имеет один серьезный недостаток: он не гарантирует правильное
выравнивание. Например, что-то вроде:

struct Hack
{
char size;
int* buffer() { return reinterpret_cast<int*>(this + 1); }
};

вероятно, вернет неправильно выровненный указатель. Вы можете обойти
это путем помещения данных в структуре в союз с типом
чей указатель вы возвращаете. Если у вас есть C ++ 11, вы можете
заявляет:

struct alignas(alignof(int)) Hack
{
char size;
int* buffer() { return reinterpret_cast<int*>(this + 1); }
};

(Я думаю. Я на самом деле никогда не пробовал это, и я мог бы иметь
детали синтаксиса неверны.)

Эта идиома имеет второй важный недостаток: она ничего не делает для
убедитесь, что поле размера соответствует фактическому размеру
буфер, и что еще хуже, нет реального способа использования new Вот. к
исправить это, в некоторой степени, вы можете определить конкретный класс
operator new а также operator delete:

struct alignas(alignof(int)) Hack
{
void* operator new( size_t, size_t n );
void operator delete( void* );
Hack( size_t n );
char size;
int* buffer() { return reinterpret_cast<int*>(this + 1); }
};

Затем клиентский код должен будет использовать новое размещение для выделения:

Hack* hack = new (20) Hack(20);

Клиент все еще должен повторить размер, но он не может игнорировать
Это.

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

struct alignas(alignof(int)) Hack
{
private:
void operator delete( void* p )
{
::operator delete( p );
}
//  ban all but dynamic lifetime (and also inheritance, member, etc.)
~Hack() = default;

//  ban arrays
void* operator new[]( size_t ) = delete;
void operator delete[]( void* p ) = delete;
public:
Hack( size_t n );
void* operator new( size_t, size_t n )
{
return ::operator new( sizeof(Hack) + n * sizeof(int) );
}
char size;
//  Since dtor is private, we need this.
void deleteMe() { delete this; }
int* buffer() { return reinterpret_cast<int*>(this + 1); }
};

Учитывая основные опасности такого класса, это спорно
если так много защитных мер необходимы. Даже с ними,
это действительно только для использования кем-то, кто полностью понимает все
ограничения и внимательно обращать внимание. Во всех кроме
в крайнем случае, в коде очень низкого уровня, вы просто сделаете
буфер std::vector<int> и покончим с этим. Во всех, кроме
код самого низкого уровня, разница в производительности не будет
стоит риска и усилий.

РЕДАКТИРОВАТЬ:

В качестве примера, реализация g ++
std::basic_string использует что-то очень похожее на вышесказанное,
с struct содержащий счетчик ссылок, текущий размер
и текущая мощность (три size_t), а затем непосредственно
буфер символов. И так как это было написано задолго до
С ++ 11 и alignas/alignof, что-то вроде
std::basic_string<double> произойдет сбой в некоторых системах (например,
Sparc). (Хотя технически это ошибка, большинство людей не считают
это критическая проблема.)

10

Другие решения

Это C ++, поэтому шаблоны доступны:

template <int N>
struct hack {
int filler;
int thing [N];
};

В таком случае трудной задачей будет приведение между указателями на разные экземпляры.

10

Первое, что приходит на ум, это не надо, не пишите C на C ++. В 99,99% случаев это hack не требуется, не принесет заметного улучшения производительности по сравнению с std::vector и усложнит вашу жизнь и жизнь других сопровождающих проекта, в котором вы это внедрили.

Если вы хотите использовать стандартный совместимый подход, предоставьте тип оболочки, который динамически выделяет кусок памяти, достаточно большой, чтобы содержать hack (минус массив) плюс N*sizeof(int) для эквивалента массива (не забудьте обеспечить правильное выравнивание). Класс будет иметь средства доступа, которые отображают члены и элементы массива в правильное расположение в памяти.

Игнорирование выравнивания и кода котельной пластины, чтобы сделать интерфейс красивым и безопасным для реализации:

template <typename T>
class DataWithDynamicArray {
void *ptr;
int* array() {
return static_cast<int*>(static_cast<char*>(ptr)+sizeof(T)); // align!
}
public:
DataWithDynamicArray(int size) : ptr() {
ptr = malloc(sizeof(T) + sizeof(int)*size); // force correct alignment
new (ptr) T();
}
~DataWithDynamicArray() {
static_cast<T*>(ptr)->~T();
free(ptr);
}
// copy, assignment...
int& operator[](int pos) {
return array()[pos];
}
T& data() {
return *static_cast<T*>(ptr);
}
};

struct JustSize { int size; };
DataWithDynamicArray<JustSize> x(10);
x.data().size = 10
for (int i = 0; i < 10; ++i) {
x[i] = i;
}

Теперь я бы на самом деле не реализовал это таким образом (я бы вообще этого не делал !!), так как, например, размер должен быть частью состояния DataWithDynamicArray

Этот ответ предоставляется только в качестве упражнения, чтобы объяснить, что то же самое можно сделать без расширений, но будьте осторожны, это всего лишь игрушечный пример, который имеет много проблемы, включая, но не ограничиваясь, исключение безопасности или выравнивания (и все же лучше, чем принуждение пользователя сделать malloc с правильным размером). Тот факт, что вы Можно не значит что ты должен, и реальный вопрос в том, необходимость эта функция и то, что вы пытаетесь сделать, это хороший дизайн или нет.

8

Если вы действительно чувствуете необходимость использовать взлом, почему бы просто не использовать

struct hack {
char filler;
int things[1];
};

с последующим

hack_p = malloc(sizeof(struct hack)+(N-1)*sizeof int));

Или даже не беспокойтесь о -1 и живите с небольшим дополнительным пространством.

4

В C ++ нет понятия «гибкие массивы». Единственный способ иметь гибкий массив в C ++ — это использовать динамический массив — что заставляет вас использовать int* things, Вам понадобится параметр размера, если вы пытаетесь прочитать эти данные из файла, чтобы вы могли создать массив соответствующего размера (или использовать std::vector и просто продолжайте читать, пока не дойдете до конца потока).

Хак с «гибким массивом» сохраняет пространственную локальность (то есть имеет выделенную память в непрерывном блоке для остальной части структуры), которую вы теряете, когда вынуждены использовать динамическую память. На самом деле не существует элегантного способа обойти это (например, вы могли бы выделить большой буфер, но вам нужно было бы сделать его достаточно большим, чтобы вместить любое количество элементов, которое вы хотите — и если фактические считываемые данные были меньше, чем буфер, будет выделено место впустую).

Кроме того, прежде чем люди начнут предлагать хранить int * отдельно
выделенная часть памяти в структуре, это не
удовлетворительный ответ. Я хочу выделить один кусок памяти для
держать и мою структуру и элементы массива. Использование std :: vector также
попадает в ту же категорию.

Тот является так, как вы бы сделали это в C ++. Вы можете отклонить все, что хотите, но факт остается фактом: нестандартное расширение не сработает, когда вы перейдете к компилятору, который его не поддерживает. Если вы придерживаетесь стандарта (например, избегаете использования хаков, специфичных для компилятора), вы с меньшей вероятностью столкнетесь с этими типами проблем.

3

Существует по крайней мере одно преимущество для гибких элементов массива по сравнению с массивами нулевой длины, когда компилятор является Clang.

struct Strukt1 {
int fam[];
int size;
};

struct Strukt2 {
int fam[0];
int size;
};

Здесь лязг будет ошибка, если он видит Strukt1 но не ошибется, если увидит Strukt2, gcc и icc принимают либо без ошибок, либо с ошибками msvc в любом случае. gcc выдает ошибку, если код скомпилирован как C.

То же самое относится и к этому похожему, но менее очевидному примеру:

struct Strukt3 {
int size;
int fam[];
};

strukt Strukt4 {
Strukt3 s3;
int i;
};
1
По вопросам рекламы [email protected]