Подтвердить что ты не робот

Структурный взломать эквивалент в С++

Структурный хак, в котором у вас есть массив длины 0 в качестве последнего члена структуры из C90 и C99, хорошо известен, и с введением гибких членов массива в C99 мы даже получили стандартизованный способ его использования с []. К сожалению, С++ не предоставляет такой конструкции и (по крайней мере, с помощью 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[];

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

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

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

4b9b3361

Ответ 1

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

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

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

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

скорее всего, вернет неверный указатель. Вы можете работать это, поместив данные в структуру в объединение с типом указатель которого вы возвращаете. Если у вас есть С++ 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); }
};

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

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); }
};

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

EDIT:

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

Ответ 2

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

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

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

Ответ 3

Первое, что приходит в голову, - DO не, не писать 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 с правильным размером). Тот факт, что вы можете не означает, что вы должны, и реальный вопрос заключается в том, нужна ли вам эта функция и то, что вы пытаетесь do - хороший дизайн вообще или нет.

Ответ 4

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

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

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

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

Ответ 5

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

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

за которым следует

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

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

Ответ 6

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

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

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

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

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

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

strukt Strukt4 {
    Strukt3 s3;
    int i;
};