Копіювання конструкцій з неініціалізованими членами


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

ну .. що структура не є ПОД (Прості старі дані)? Це означає, що члени будуть ініціалізовані зі значеннями за замовчуванням? Це сумніви
Кевін Кукетсу

Чи не в цьому випадку це неглибока копія? що може піти не так у цьому випадку, якщо не скористатися неініціалізованим членом у скопійованій структурі?
TruthSeeker

@KevinKouketsu Я додав умову для випадку, коли потрібен тривіальний / POD тип.
волоський горіх

@TruthSeeker Стандарт говорить, що це невизначена поведінка. Причина, як правило, невизначена поведінка для змінних (не членів), пояснюється у відповіді Андрея Семашева. В основному це підтримувати уявлення про пастку з неініціалізованою пам'яттю. Чи призначено це застосувати до неявної копії побудови конструкцій - це питання пов'язаного питання CWG.
волоський горіх

@TruthSeeker Конструктор неявної копії визначений для копіювання кожного члена окремо, як би шляхом прямої ініціалізації. Не визначено копіювання представлення об'єкта так, як якщо б memcpy, навіть для тривіально копіюваних типів. Єдиним винятком є ​​об'єднання, для яких конструктор неявної копії копіює представлення об'єкта так, ніби на memcpy.
волоський горіх

11

Взагалі, копіювання неініціалізованих даних є невизначеною поведінкою, оскільки ці дані можуть знаходитись у захопленому стані. Цитуючи цю сторінку:

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

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

Однак для тривіально копіюваних типів можна memcpyскопіювати необроблене подання об'єкта. Це зробити безпечно, оскільки значення об'єкта не інтерпретується, а натомість копіюється необроблена послідовність байтів представлення об'єкта.


Що щодо даних типів, для яких всі бітові шаблони представляють дійсні значення (наприклад, 64-байтова структура, що містить an unsigned char[64])? Трактування байтів структури як невказаних значень може безперешкодно перешкоджати оптимізації, але вимагати від програмістів вручну заповнювати масив з марними значеннями, ще більше заважатиме ефективність.
supercat

Ініціалізація даних не є марною, вона перешкоджає UB, незалежно від того, спричинені вони представниками пасток або згодом із використанням неініціалізованих даних. Нульовий обсяг 64 байт (1 або 2 лінії кешу) не такий дорогий, як може здатися. А якщо у вас є великі структури, де це дорого, варто подумати двічі, перш ніж копіювати їх. І я впевнений, що вам доведеться ініціалізувати їх у будь-який момент.
Андрій Семашев

Операції машинного коду, які не можуть вплинути на поведінку програми, марні. Думка про те, що будь-яку дію, охарактеризовану стандартом UB, слід уникати будь-якою ціною, а говорити про те, що [словами Комітету зі стандартів C] UB "визначає області можливого відповідного розширення мови", є порівняно недавнім. Хоча я не бачив опублікованого обгрунтування стандарту C ++, він прямо відмовляється від юрисдикції щодо того, які програми C ++ "дозволено" робити, відмовляючись класифікувати програми як відповідні або невідповідні, а це означає, що це дозволить подібні розширення.
supercat

-1

У деяких випадках, наприклад описаний, стандарт C ++ дозволяє компіляторам обробляти конструкції будь-яким способом, яким їхні клієнти вважатимуть найбільш корисними, не вимагаючи, щоб така поведінка була передбачуваною. Іншими словами, такі конструкції викликають "Невизначене поведінку". Однак це не означає, що такі конструкції мають бути «забороненими», оскільки стандарт C ++ явно відмовляється від юрисдикції щодо того, які добре сформовані програми можуть «робити». Хоча я не знаю жодного опублікованого документа з обґрунтуванням стандарту C ++, той факт, що він описує не визначене поведінку, як це робиться у 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 ++ не робить спроб сказати, що будь-яка з цих форм поведінки є достатньо корисною, ніж інша, щоб виправдати її обов'язок. Як не дивно, ця відсутність специфікацій може бути призначена для полегшення оптимізації, але якщо програмісти не зможуть використовувати будь-які слабкі гарантії поведінки, будь-які оптимізації будуть заперечуватись.


-2

Оскільки всі члени групи Dataпримітивних типів, data2отримають точну "побітну копію" всіх членів data. Таким чином, значення data2.bбуде точно таким же, як значення data.b. Однак точного значення data.bнеможливо передбачити, оскільки ви не ініціалізували його явно. Це залежатиме від значень байтів у області пам'яті, виділеної для data.


Чи можете ви підтримати це посиланням на стандарт? Посилання, надані @walnut, означають, що це не визначене поведінка. Чи є у стандарті виняток для POD?
Tomek Czajka

Хоча наступне не посилається на стандарт, все-таки: en.cppreference.com/w/cpp/language/… "Тривіальнокопіювані об'єкти можна скопіювати, скопіювавши їхні представлення об'єктів вручну, наприклад, з std :: memmove. Усі типи даних, сумісні з C мова (типи POD) можна скопіювати ".
ivan.ukr

Єдине "невизначене поведінка" в цьому випадку полягає в тому, що ми не можемо передбачити значення неініціалізованої змінної члена. Але код компілюється і працює успішно.
ivan.ukr

1
Фрагмент, який ви цитуєте, розповідає про поведінку memmove, але це не дуже актуально, оскільки в коді я використовую конструктор копій, а не memmove. З інших відповідей випливає, що використання конструктора копіювання призводить до невизначеної поведінки. Я думаю, ви також неправильно розумієте термін "невизначена поведінка". Це означає, що мова взагалі не дає гарантій, наприклад, програма може випадково вибивати або пошкоджувати дані або робити що-небудь. Це не означає, що якесь значення є непередбачуваним, це було б не визначеною поведінкою.
Tomek Czajka

@ ivan.ukr Стандарт C ++ вказує на те, що неявні конструктори копіювання / переміщення діють як член, як би шляхом прямої ініціалізації, див. посилання у моїй відповіді. Тому конструкція копії не робить " " бітову копію " ". Ви правильні лише для типів об'єднання, для яких вказано неявний конструктор копіювання , щоб скопіювати представлення об'єкта так, ніби в посібнику std::memcpy. Ніщо з цього не заважає використовувати std::memcpyабо std::memmove. Це лише запобігає використанню неявного конструктора копій.
волоський горіх
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.