Безопасен ли этот трюк с инициализацией структуры C ++?

StackOverflow https://stackoverflow.com/questions/112085

  •  02-07-2019
  •  | 
  •  

Вопрос

Вместо того, чтобы помнить о необходимости инициализации простой структуры "C", я мог бы извлечь из нее и обнулить ее в конструкторе следующим образом:

struct MY_STRUCT
{
    int n1;
    int n2;
};

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct()
    {
        memset(this, 0, sizeof(MY_STRUCT));
    }
};

Этот трюк часто используется для инициализации структур Win32 и иногда может задавать повсеместные Размер cbSize Участник.

Теперь, пока нет таблицы виртуальных функций для уничтожения вызовом memset, является ли это безопасной практикой?

Это было полезно?

Решение

ПРЕАМБУЛА:

Хотя мой ответ все еще в порядке, я нахожу ответ litb значительно превосходит мой, потому что:

  1. Это учит меня трюку, которого я не знал (ответы litb обычно имеют такой эффект, но я записываю это впервые).
  2. Это точно отвечает на вопрос (то есть инициализирует нулевую часть исходной структуры).

Поэтому, пожалуйста, рассмотрите ответ litb раньше моего.На самом деле, я предлагаю автору вопроса считать ответ litb правильным.

Оригинальный ответ

Ввод истинного объекта (т. е.std::строка) и т.д.inside сломается, потому что истинный объект будет инициализирован перед memset, а затем перезаписан нулями.

Использование списка инициализации не работает для g ++ (я удивлен ...).Вместо этого инициализируйте его в теле конструктора CMyStruct.Он будет дружественен к C ++:

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct() { n1 = 0 ; n2 = 0 ; }
};

P.S.:Я предполагал, что у вас действительно есть НЕТ контроль над MY_STRUCT, конечно.С control вы бы добавили конструктор непосредственно внутри MY_STRUCT и забыли о наследовании.Обратите внимание, что вы можете добавить невиртуальные методы в C-подобную структуру и при этом заставить ее вести себя как struct.

Редактировать:Добавил недостающую скобку после комментария Лу Франко.Спасибо!

ПРАВКА 2 :Я попробовал код на g ++, и по какой-то причине использование списка инициализации не работает.Я исправил код с помощью конструктора body.Тем не менее, решение все еще остается в силе.

Пожалуйста, пересмотрите мой пост, так как исходный код был изменен (см. Список изменений для получения дополнительной информации).

ПРАВКА 3 :Прочитав комментарий Роба, я предполагаю, что у него есть точка зрения, достойная обсуждения:"Согласен, но это может быть огромная структура Win32, которая может измениться с новым SDK, поэтому memset - это доказательство будущего ".

Я не согласен:Зная Microsoft, можно сказать, что это не изменится из-за их потребности в идеальной обратной совместимости.Вместо этого они создадут расширенную MY_STRUCTБывший структура с тем же начальным макетом, что и MY_STRUCT, с дополнительными элементами в конце и узнаваемая по переменной-члену "size", подобной структуре, используемой для RegisterWindow, IIRC.

Таким образом, единственный верный момент, оставшийся от комментария Роба, - это "огромная" структура.В этом случае, возможно, memset более удобен, но вам придется сделать MY_STRUCT переменным членом CMyStruct вместо наследования от него.

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

ПРАВКА 4:Пожалуйста, взгляните на решение Фрэнка Крюгера.Я не могу обещать, что это переносимо (я предполагаю, что это так), но это все еще интересно с технической точки зрения, потому что это показывает один случай, когда в C ++ указатель "this" "address" перемещается из своего базового класса в свой унаследованный класс.

Другие советы

Вы можете просто инициализировать базу по значению, и все ее элементы будут обнулены.Это гарантировано

struct MY_STRUCT
{
    int n1;
    int n2;
};

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct():MY_STRUCT() { }
};

Чтобы это сработало, в базовом классе не должно быть пользовательского объявленного конструктора, как в вашем примере.

Никакой гадости memset за это.Это не гарантирует, что memset работает в вашем коде, хотя это должно работать на практике.

Намного лучше, чем memset, вы можете использовать вместо этого этот маленький трюк:

MY_STRUCT foo = { 0 };

Это инициализирует все элементы равными 0 (или их значению по умолчанию iirc), нет необходимости указывать значение для каждого.

Это заставило бы меня чувствовать себя намного безопаснее, так как это должно сработать, даже если есть vtable (или компилятор будет кричать).

memset(static_cast<MY_STRUCT*>(this), 0, sizeof(MY_STRUCT));

Я уверен, что ваше решение сработает, но я сомневаюсь, что при смешивании можно дать какие-либо гарантии memset и занятия.

Это прекрасный пример переноса идиомы C на C ++ (и почему это может не всегда работать ...)

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

Таким образом, что, если позже какой-нибудь благонамеренный программист изменит MY_STRUCT следующим образом:


struct MY_STRUCT
{
    int n1;
    int n2;

   // Provide a default implementation...
   virtual int add() {return n1 + n2;}  
};

Добавив эту единственную функцию, ваш memset теперь может привести к хаосу.Подробное обсуждение приведено в комп.язык.c+

Примеры имеют "неопределенное поведение".

Для не-POD порядок, в котором компилятор размещает объект (все базовые классы и члены), не указан (ISO C ++ 10/3).Рассмотрим следующее:

struct A {
  int i;
};

class B : public A {       // 'B' is not a POD
public:
  B ();

private:
  int j;
};

Это может быть изложено следующим образом:

[ int i ][ int j ]

Или как:

[ int j ][ int i ]

Следовательно, использование memset непосредственно по адресу "this" - это очень неопределенное поведение.Один из приведенных выше ответов, на первый взгляд, выглядит более безопасным:

 memset(static_cast<MY_STRUCT*>(this), 0, sizeof(MY_STRUCT));

Однако я считаю, что, строго говоря, это тоже приводит к неопределенному поведению.Я не могу найти нормативный текст, однако в примечании к 10/5 говорится:"Подобъект базового класса может иметь макет (3.7), отличный от макета большинства производных объектов того же типа".

В результате мой компилятор мог выполнять оптимизацию пространства с различными элементами:

struct A {
  char c1;
};

struct B {
  char c2;
  char c3;
  char c4;
  int i;
};

class C : public A, public B
{
public:
  C () 
  :  c1 (10);
  {
    memset(static_cast<B*>(this), 0, sizeof(B));      
  }
};

Может быть изложен в виде:

[ char c1 ] [ char c2, char c3, char c4, int i ]

В 32-битной системе, из-за сбоев и т.д.для 'B' размер (B), скорее всего, будет равен 8 байтам.Однако sizeof (C) также может составлять "8" байт, если компилятор упаковывает элементы данных.Следовательно, вызов memset может перезаписать значение, присвоенное 'c1'.

Точное расположение класса или структуры в C ++ не гарантируется, вот почему вы не должны делать предположений о его размере извне (это означает, что если вы не компилятор).

Вероятно, это работает, пока вы не найдете компилятор, в котором этого нет, или не добавите в него какую-нибудь виртуальную таблицу.

Если у вас уже есть конструктор, почему бы просто не инициализировать его там с помощью n1 = 0;n2=0;-- это, конечно, тем более Нормальный образом.

Редактировать:На самом деле, как показал paercebal, инициализация ctor еще лучше.

Мое мнение - нет.Я тоже не уверен, что это даст.

Поскольку ваше определение CMyStruct изменяется и вы добавляете / удаляете участников, это может привести к ошибкам.Запросто.

Создайте конструктор для CMyStruct, который принимает параметр MyStruct has.

CMyStruct::CMyStruct(MyStruct &)

Или что-то в этом роде искал.Затем вы можете инициализировать общедоступный или закрытый элемент "MyStruct".

С точки зрения ISO C ++, есть две проблемы:

(1) Является ли объект модулем?Аббревиатура расшифровывается как Plain Old Data, а стандарт перечисляет то, чего у вас не может быть в МОДУЛЕ (в Википедии есть хорошее резюме).Если это не модуль, вы не можете его запомнить.

(2) Существуют ли члены, для которых all-bits-zero недопустим?В Windows и Unix нулевой указатель равен нулю во всех битах;в этом нет необходимости.С плавающей запятой 0 все биты равны нулю в IEEE754, что довольно распространено, и на x86.

Совет Фрэнка Крюгерса устраняет ваши опасения, ограничивая memset базой POD класса, отличного от POD.

Попробуйте это - перегрузите новое.

Редактировать:Я должен добавить - Это безопасно, потому что память обнуляется раньше Любой вызываются конструкторы.Большой недостаток - работает только в том случае, если объект выделяется динамически.

struct MY_STRUCT
{
    int n1;
    int n2;
};

class CMyStruct : public MY_STRUCT
{
public:
    CMyStruct()
    {
        // whatever
    }
    void* new(size_t size)
    {
        // dangerous
        return memset(malloc(size),0,size);
        // better
        if (void *p = malloc(size))
        {
            return (memset(p, 0, size));
        }
        else
        {
            throw bad_alloc();
        }
    }
    void delete(void *p, size_t size)
    {
        free(p);
    }

};

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

struct MY_STRUCT
{
  int n1;
  int n2;
  MY_STRUCT(): n1(0), n2(0) {}
};

Я не уверен насчет эффективности, но я ненавижу выкидывать фокусы, когда вы еще не доказали, что эффективность необходима.

Прокомментируйте ответ litb (кажется, мне пока не разрешают комментировать напрямую):

Даже с этим приятным решением в стиле C ++ вы должны быть очень осторожны, чтобы не применять это наивно к структуре, содержащей элемент, не являющийся POD.

После этого некоторые компиляторы больше не инициализируются корректно.

Видишь это ответ на аналогичный вопрос.У меня лично был неудачный опыт работы с VC2008 с дополнительным std::string.

Что я делаю, так это использую агрегатную инициализацию, но указываю только инициализаторы для участников, которые мне небезразличны, например:

STARTUPINFO si = {
    sizeof si,      /*cb*/
    0,              /*lpReserved*/
    0,              /*lpDesktop*/
    "my window"     /*lpTitle*/
};

Остальные члены будут инициализированы нулями соответствующего типа (как в сообщении Drealmer).Здесь вы доверяете Microsoft, которая не будет необоснованно нарушать совместимость, добавляя новые элементы структуры посередине (разумное предположение).Это решение кажется мне оптимальным - один оператор, никаких классов, никакого memset, никаких предположений о внутреннем представлении нулевых или нулевых указателей с плавающей запятой.

Я думаю, что взломы, связанные с наследованием, - это ужасный стиль.Публичное наследование означает IS-A для большинства читателей.Обратите также внимание, что вы наследуете от класса, который не предназначен для использования в качестве базового.Поскольку виртуального деструктора нет, клиенты, которые удаляют экземпляр производного класса через указатель на base, будут вызывать неопределенное поведение.

Я предполагаю, что структура предоставлена вам и не может быть изменена.Если вы можете изменить структуру, то очевидным решением будет добавление конструктора.

Не перегружайте свой код оболочками C ++, когда все, что вам нужно, - это простой макрос для инициализации вашей структуры.

#include <stdio.h>

#define MY_STRUCT(x) MY_STRUCT x = {0}

struct MY_STRUCT
{
    int n1;
    int n2;
};

int main(int argc, char *argv[])
{
    MY_STRUCT(s);

    printf("n1(%d),n2(%d)\n", s.n1, s.n2);

    return 0;
}

Это немного кода, но его можно использовать повторно;включите его один раз, и он должен работать для любого модуля.Вы можете передать экземпляр этого класса любой функции, ожидающей MY_STRUCT , или использовать функцию GetPointer, чтобы передать его в функцию, которая изменит структуру.

template <typename STR>
class CStructWrapper
{
private:
    STR MyStruct;

public:
    CStructWrapper() { STR temp = {}; MyStruct = temp;}
    CStructWrapper(const STR &myStruct) : MyStruct(myStruct) {}

    operator STR &() { return MyStruct; }
    operator const STR &() const { return MyStruct; }

    STR *GetPointer() { return &MyStruct; }
};

CStructWrapper<MY_STRUCT> myStruct;
CStructWrapper<ANOTHER_STRUCT> anotherStruct;

Таким образом, вам не нужно беспокоиться о том, являются ли все значения NULL равными 0 или представлениями с плавающей запятой.Пока STR является простым агрегатным типом, все будет работать.Когда STR не является простым агрегированным типом, вы получите ошибку времени компиляции, так что вам не придется беспокоиться о случайном неправильном использовании этого параметра.Кроме того, если тип содержит что-то более сложное, пока у него есть конструктор по умолчанию, все в порядке:

struct MY_STRUCT2
{
    int n1;
    std::string s1;
};

CStructWrapper<MY_STRUCT2> myStruct2; // n1 is set to 0, s1 is set to "";

С другой стороны, это медленнее, поскольку вы создаете дополнительную временную копию, и компилятор присвоит каждому элементу значение 0 по отдельности, вместо одного memset.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top