Является ли эта инициатива инициализации структуры С++ безопасной?
Вместо того, чтобы помнить, чтобы инициализировать простую структуру "C", я мог бы извлечь из нее и обнулить ее в конструкторе следующим образом:
struct MY_STRUCT
{
int n1;
int n2;
};
class CMyStruct : public MY_STRUCT
{
public:
CMyStruct()
{
memset(this, 0, sizeof(MY_STRUCT));
}
};
Этот трюк часто используется для инициализации структур Win32 и иногда может устанавливать вездесущий элемент cbSize.
Теперь, если для вызова memset для уничтожения нет таблицы виртуальных функций, это безопасная практика?
Ответы
Ответ 1
Преамбула:
В то время как мой ответ по-прежнему хорошо, я нахожу litb answer, намного превосходящий мой, потому что:
- Он учит меня трюку, который я не знал (ответы на лабле, как правило, имеют этот эффект, но это первый раз, когда я его записываю)
- Он отвечает именно на вопрос (то есть инициализирует исходную часть структуры до нуля)
Так что, пожалуйста, подумайте о лифте перед моим. На самом деле, я предлагаю автору вопроса рассмотреть вопрос о литовском ответе как о правильном.
Оригинальный ответ
Помещение истинного объекта (т.е. std::string) и т.д. внутри будет ломаться, потому что истинный объект будет инициализирован до memset, а затем перезаписан нулями.
Использование списка инициализации не работает для g++ (я удивлен...). Инициализируйте его в теле конструктора CMyStruct. Это будет С++ дружественным:
class CMyStruct : public MY_STRUCT
{
public:
CMyStruct() { n1 = 0 ; n2 = 0 ; }
};
P.S.: Я предположил, что у вас действительно есть no контроль над MY_STRUCT. С контролем вы бы добавили конструктор непосредственно в MY_STRUCT и забыли о наследовании. Обратите внимание, что вы можете добавлять не виртуальные методы в C-подобную структуру и все еще вести себя как структура.
EDIT: Добавлена отсутствующая скобка, после комментария Лу Франко. Спасибо!
EDIT 2: я пробовал код на g++, и по какой-то причине использование списка инициализации не работает. Я исправил код, используя конструктор body. Решение остается в силе, однако.
Пожалуйста, переоцените мой пост, поскольку исходный код был изменен (см. журнал изменений для получения дополнительной информации).
РЕДАКТИРОВАТЬ 3: После прочтения комментария Роба, я думаю, у него есть точка, достойная обсуждения: "Согласовано, но это может быть огромная структура Win32, которая может измениться с помощью нового SDK, поэтому memset является будущим доказательством".
Я не согласен: знай Microsoft, это не изменится из-за их потребности в идеальной обратной совместимости. Они создадут вместо этого расширенную структуру MY_STRUCT Ex с тем же первоначальным расположением, что и MY_STRUCT, с добавочными членами в конце и распознаваемыми с помощью переменной члена "size", такой как структура, используемая для RegisterWindow, IIRC.
Таким образом, единственная действительная точка, остающаяся от комментария Роба, - это "огромная" структура. В этом случае, возможно, memset более удобен, но вы должны сделать MY_STRUCT переменным членом CMyStruct вместо его наследования.
Я вижу еще один хак, но я думаю, что это сломается из-за возможной проблемы выравнивания структуры.
EDIT 4: Пожалуйста, взгляните на решение Фрэнка Крюгера. Я не могу пообещать, что это портативный (я думаю, это так), но он по-прежнему интересен с технической точки зрения, потому что он показывает один случай, когда на С++ адрес указателя "this" "переходит" от базового класса к его унаследованному классу.
Ответ 2
Вы можете просто инициализировать базу значений, и все ее члены будут нулевыми. Это гарантировано
struct MY_STRUCT
{
int n1;
int n2;
};
class CMyStruct : public MY_STRUCT
{
public:
CMyStruct():MY_STRUCT() { }
};
Для этого в базовом классе не должно быть объявленного конструктора пользователя, как в вашем примере.
Нет противного memset
для этого. Это не гарантировало, что memset
работает в вашем коде, хотя он должен работать на практике.
Ответ 3
Гораздо лучше, чем memset, вы можете использовать этот маленький трюк:
MY_STRUCT foo = { 0 };
Это приведет к инициализации всех членов до 0 (или их значения по умолчанию iirc), не нужно указывать значение для каждого.
Ответ 4
Это заставило бы меня чувствовать себя намного безопаснее, поскольку оно должно работать, даже если есть vtable
(или компилятор будет кричать).
memset(static_cast<MY_STRUCT*>(this), 0, sizeof(MY_STRUCT));
Я уверен, что ваше решение будет работать, но я сомневаюсь, что при смешивании memset
и классов есть какие-то гарантии.
Ответ 5
Это прекрасный пример переноса идиомы C на С++ (и почему это может не всегда работать...)
Проблема с использованием memset заключается в том, что в С++ структура и класс - это одно и то же, кроме того, что по умолчанию структура имеет общедоступную видимость, а класс имеет конфиденциальную видимость.
Таким образом, что, если позже, какой-то хорошо программирующий программист меняет MY_STRUCT так:
struct MY_STRUCT
{
int n1;
int n2;
// Provide a default implementation...
virtual int add() {return n1 + n2;}
};
Добавив эту единственную функцию, ваш memset может теперь вызвать хаос.
Подробное обсуждение в comp.lang.c+
Ответ 6
В примерах есть "неуказанное поведение".
Для не-POD порядок, с помощью которого компилятор выставляет объект (все базовые классы и члены), не указан (ISO С++ 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" sizeof (B), скорее всего, будет 8 байтов. Однако sizeof (C) также может быть "8" байтами, если компилятор упаковывает данные. Поэтому вызов memset может перезаписать значение, указанное в 'c1'.
Ответ 7
Точная компоновка класса или структуры не гарантируется в С++, поэтому вы не должны делать предположений о ее размере извне (это означает, что вы не являетесь компилятором).
Возможно, это работает, пока вы не найдете компилятор, на котором это не так, или вы выбросите в таблицу какую-нибудь vtable.
Ответ 8
Если у вас уже есть конструктор, почему бы просто не инициализировать его там, где n1 = 0; п2 = 0; - это, безусловно, более нормальный путь.
Изменить: На самом деле, как показал paercebal, инициализация ctor еще лучше.
Ответ 9
Мое мнение - нет. Я не уверен, что он получает.
По мере изменения вашего определения CMyStruct и добавления/удаления членов это может привести к ошибкам. Легко.
Создайте конструктор для CMyStruct, который принимает MyStruct, имеет параметр.
CMyStruct::CMyStruct(MyStruct &)
Или что-то от этого искалось. Затем вы можете инициализировать публичный или закрытый элемент "MyStruct".
Ответ 10
С точки зрения ISO С++ есть две проблемы:
(1) Является ли объект POD? Аббревиатура обозначает Plain Old Data, а в стандарте перечислены то, что вы не можете иметь в POD (у Википедии есть хорошее резюме). Если это не POD, вы не можете его изменить.
(2) Существуют ли члены, для которых все биты-ноль недействительны? В Windows и Unix указатель NULL - это все биты нуль; это не должно быть. Плавающая точка 0 имеет все биты нуля в IEEE754, что довольно часто, и на x86.
Фрэнк Крюгерс обсуждает ваши проблемы, ограничивая memset базой POD класса, отличного от POD.
Ответ 11
Попробуйте это - перегрузите новый.
EDIT: я должен добавить: это безопасно, потому что память обнуляется перед вызовом любых конструкторов. Большая ошибка - работает только если объект динамически распределен.
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);
}
};
Ответ 12
Если MY_STRUCT является вашим кодом, и вы довольны использованием компилятора С++, вы можете поместить конструктор туда без упаковки в класс:
struct MY_STRUCT
{
int n1;
int n2;
MY_STRUCT(): n1(0), n2(0) {}
};
Я не уверен в эффективности, но я ненавижу делать трюки, когда вам не нужна эффективность.
Ответ 13
Комментарий litb answer (кажется, мне еще не разрешено прокомментировать):
Даже с помощью этого приятного решения в стиле С++ вы должны быть очень осторожны, чтобы вы не применили это наивно к структуре, содержащей член не-POD.
Некоторые компиляторы затем не инициализируются правильно.
См. этот ответ на аналогичный вопрос.
У меня лично был плохой опыт на VC2008 с дополнительным std::string.
Ответ 14
Это немного кода, но он многоразовый; включите его один раз, и он должен работать для любого POD. Вы можете передать экземпляр этого класса любой функции, ожидающей 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 "";
С другой стороны, это медленнее, так как вы делаете дополнительную временную копию, и компилятор назначит каждому члену по отдельности, вместо одного memset.
Ответ 15
Я использую агрегатную инициализацию, но только указываю инициализаторы для членов, которых интересует, например:
STARTUPINFO si = {
sizeof si, /*cb*/
0, /*lpReserved*/
0, /*lpDesktop*/
"my window" /*lpTitle*/
};
Остальные члены будут инициализированы нулями соответствующего типа (как в столбце Drealmer). Здесь вы доверяете Microsoft не безвозмездно нарушать совместимость, добавляя в нее новых членов структуры (разумное предположение). Это решение кажется мне оптимальным - один оператор, никакие классы, не memset, никаких предположений о внутреннем представлении нулей с плавающей точкой или нулевых указателях.
Я думаю, что взломы с наследованием - ужасный стиль. Публичное наследование означает IS-A для большинства читателей. Также обратите внимание, что вы наследуете класс, который не предназначен для базы. Поскольку виртуального деструктора нет, клиенты, которые удаляют экземпляр производного класса с помощью указателя на базу, будут ссылаться на поведение undefined.
Ответ 16
Я предполагаю, что структура предоставлена вам и не может быть изменена. Если вы можете изменить структуру, то очевидное решение добавляет конструктор.
Не перестраивайте свой код с помощью оберток С++, когда все, что вам нужно, это простой макрос для инициализации вашей структуры.
#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;
}