Копирование структур с неинициализированными членами

29

Допустимо ли копировать структуру, некоторые члены которой не инициализированы?

Я подозреваю, что это неопределенное поведение, но если это так, то оставлять неинициализированные члены в структуре (даже если эти члены никогда не используются напрямую) довольно опасно. Поэтому мне интересно, есть ли что-то в стандарте, что позволяет это.

Например, это действительно?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
Томек Цайка
источник
Я помню, как видел похожий вопрос некоторое время назад, но не могу его найти. Этот вопрос связан, как и этот .
1201ProgramAlarm

Ответы:

23

Да, если неинициализированный член не является беззнаковым узким символьным типом или std::byte, то копирование структуры, содержащей это неопределенное значение, с помощью неявно определенного конструктора копирования является технически неопределенным поведением, так как оно предназначено для копирования переменной с неопределенным значением того же типа, поскольку из [dcl.init] / 12 .

Это применимо здесь, потому что неявно сгенерированный конструктор копирования определен, за исключением unions, для копирования каждого члена индивидуально, как если бы это было с помощью прямой инициализации, см. [Class.copy.ctor] / 4 .

Это также является предметом активной проблемы CWG 2264 .

Полагаю, на практике у вас не возникнет никаких проблем с этим.

Если вы хотите быть на 100% уверены, использование std::memcpyвсегда имеет четко определенное поведение, если тип легко копируется , даже если члены имеют неопределенное значение.


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

struct Data {
  int a{}, b{};
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
грецкий орех
источник
хорошо .. что структура не POD (простые старые данные)? Это означает, что члены будут инициализированы со значениями по умолчанию? Это сомнение
Кевин Кукетсу
Разве это не мелкая копия в этом случае? что может пойти не так с этим, если не получен доступ к неинициализированному члену в скопированной структуре?
TruthSeeker
@KevinKouketsu Я добавил условие для случая, когда требуется тривиальный тип / POD.
грецкий орех
@TruthSeeker Стандарт говорит, что это неопределенное поведение. Причина, по которой это, как правило, неопределенное поведение переменных (не являющихся членами), объясняется в ответе Андрея Семашева. По сути это поддержка представлений ловушек с неинициализированной памятью. Вопрос о том, предназначено ли это для неявного копирования конструкций, является вопросом связанной проблемы CWG.
грецкий орех
@TruthSeeker Неявный конструктор копирования определен для копирования каждого члена в отдельности, как если бы это было с помощью прямой инициализации. Он не определен для копирования представления объекта как бы memcpy, даже для тривиально копируемых типов. Единственным исключением являются союзы, для которых неявный конструктор копирования действительно копирует представление объекта, как если бы memcpy.
грецкий орех
11

Как правило, копирование неинициализированных данных является неопределенным поведением, поскольку эти данные могут находиться в состоянии захвата. Цитирую эту страницу:

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

Сигнальные NaN возможны для типов с плавающей запятой, и на некоторых платформах целые числа могут иметь представления ловушек.

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

Андрей Семашев
источник
Как насчет данных типов, для которых все битовые комбинации представляют допустимые значения (например, 64-байтовая структура, содержащая unsigned char[64])? Обработка байтов структуры как имеющих неопределенные значения может излишне затруднить оптимизацию, но требование, чтобы программисты вручную заполняли массив бесполезными значениями, еще больше снизило бы эффективность.
суперкат
Инициализация данных не бесполезна, она предотвращает UB, независимо от того, вызваны ли они представлениями прерываний или использованием неинициализированных данных в дальнейшем. Обнуление 64 байтов (1 или 2 строки кэша) не так дорого, как может показаться. И если у вас есть большие структуры, где это дорого, вы должны дважды подумать, прежде чем копировать их. И я почти уверен, что вам все равно придется их инициализировать в какой-то момент.
Андрей Семашев
Операции машинного кода, которые не могут повлиять на поведение программы, бесполезны. Представление о том, что любое действие, которое согласно Стандарту обозначено как UB, следует избегать любой ценой, скорее говоря, что [по словам Комитета по стандартам C] UB «определяет области возможного расширения соответствия языку», сравнительно недавно. Хотя я не видел опубликованного Обоснования для стандарта C ++, он явно отказывается от юрисдикции в отношении того, что «программам» на C ++ «разрешено», отказываясь классифицировать программы как соответствующие или не соответствующие, что означает, что он допускает аналогичные расширения.
суперкат
-1

В некоторых случаях, таких как описанный, стандарт C ++ позволяет компиляторам обрабатывать конструкции любым способом, который их клиенты считают наиболее полезным, не требуя, чтобы поведение было предсказуемым. Другими словами, такие конструкции вызывают «неопределенное поведение». Однако это не означает, что такие конструкции должны быть «запрещены», поскольку стандарт C ++ явно отказывается от юрисдикции в отношении того, что «правильно» выполненным программам разрешено делать. Хотя я не знаю ни одного опубликованного документа Rationale для стандарта C ++, тот факт, что он описывает Undefined Behavior во многом подобно тому, как это делает C89, наводит на мысль, что предполагаемое значение похоже: «Неопределенное поведение дает разработчику лицензию, чтобы не перехватывать определенные программные ошибки, которые являются сложными. чтобы диагностировать.

Существует много ситуаций, когда наиболее эффективный способ обработки чего-либо включает в себя написание частей структуры, которые будут заботиться о нижестоящем коде, и в то же время исключать те, которые не будут заботиться о нижестоящем коде. Требование, чтобы программы инициализировали всех членов структуры, включая тех, которые ни о чем не заботятся, без необходимости мешало бы эффективности.

Кроме того, в некоторых ситуациях может оказаться наиболее эффективным, чтобы неинициализированные данные вели себя недетерминированным образом. Например, учитывая:

struct q { unsigned char dat[256]; } x,y;

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
    temp.dat[arr[i]] = i;
  x=temp;
  y=temp;
}

если нижестоящий код не будет заботиться о значениях каких-либо элементов x.datили y.datчьи индексы не были перечислены arr, код может быть оптимизирован для:

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
  {
    int it = arr[i];
    x.dat[index] = i;
    y.dat[index] = i;
  }
}

Это повышение эффективности не было бы возможным, если бы программистам требовалось явно писать каждый элемент temp.dat, включая те, которые не интересуют нижестоящие, до его копирования.

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

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

Supercat
источник
-2

Поскольку все члены Dataимеют примитивные типы,data2 они получат точную «побитовую копию» всех членов data. Таким образом, значение data2.bбудет точно таким же, как значение data.b. Однако точное значение data.bневозможно предсказать, поскольку вы не инициализировали его явно. Это будет зависеть от значений байтов в области памяти, выделенной для data.

ivan.ukr
источник
Можете ли вы поддержать это со ссылкой на стандарт? Ссылки, предоставленные @walnut, подразумевают, что это неопределенное поведение. Есть ли исключение для POD в стандарте?
Томек Цайка
Хотя следующее не является ссылкой на стандарт, тем не менее: en.cppreference.com/w/cpp/language/… "TriviallyCopyable объекты могут быть скопированы путем копирования их представлений объектов вручную, например, с помощью std :: memmove. Все типы данных совместимы с C язык (типы POD) легко копируются. "
ivan.ukr
Единственное «неопределенное поведение» в этом случае состоит в том, что мы не можем предсказать значение неинициализированной переменной-члена. Но код компилируется и выполняется успешно.
ivan.ukr
1
Фрагмент, который вы цитируете, говорит о поведении memmove, но здесь это не очень важно, потому что в моем коде я использую конструктор копирования, а не memmove. Другие ответы подразумевают, что использование конструктора копирования приводит к неопределенному поведению. Я думаю, что вы также неправильно понимаете термин «неопределенное поведение». Это означает, что язык не дает никаких гарантий, например, программа может случайно вывести из строя или повредить данные или сделать что-нибудь еще. Это не просто означает, что какое-то значение непредсказуемо, это было бы неопределенным поведением.
Томек Цайка
@ ivan.ukr Стандарт C ++ указывает, что неявные конструкторы копирования / перемещения действуют по элементам, как будто при прямой инициализации, см. ссылки в моем ответе. Поэтому конструкция копирования не создает « побитовую копию ». Вы правильно только для типов профсоюзов, для которых неявного конструктор копирования является указанными для копирования представления объекта , как будто вручную std::memcpy. Ничто из этого не мешает использовать std::memcpyили std::memmove. Это только предотвращает использование неявного конструктора копирования.
грецкий орех