Kopiowanie struktur z niezainicjowanymi elementami

29

Czy można skopiować strukturę, której niektórzy członkowie nie są zainicjowani?

Podejrzewam, że jest to zachowanie nieokreślone, ale jeśli tak, sprawia, że ​​pozostawienie niezainicjowanych członków w strukturze (nawet jeśli ci członkowie nigdy nie są bezpośrednio wykorzystywani) jest dość niebezpieczne. Zastanawiam się więc, czy jest coś w standardzie, które to pozwala.

Na przykład, czy to jest ważne?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
Tomek Czajka
źródło
Pamiętam, że widziałem podobne pytanie jakiś czas temu, ale nie mogę go znaleźć. To pytanie jest powiązane, podobnie jak to .
1201ProgramAlarm

Odpowiedzi:

23

Tak, jeśli niezainicjowany element członkowski nie jest niepodpisanym wąskim typem znaku std::byte, wówczas kopiowanie struktury zawierającej tę nieokreśloną wartość za pomocą niejawnie zdefiniowanego konstruktora kopiowania jest technicznie nieokreślonym zachowaniem, ponieważ służy do kopiowania zmiennej o nieokreślonej wartości tego samego typu, ponieważ z [dcl.init] / 12 .

Ma to zastosowanie tutaj, ponieważ domyślnie wygenerowany konstruktor kopiowania jest unionzdefiniowany , z wyjątkiem s, w celu skopiowania każdego elementu osobno, tak jakby przez bezpośrednią inicjalizację, patrz [class.copy.ctor] / 4 .

Jest to również przedmiotem aktywnego wydania CWG 2264 .

Przypuszczam jednak, że w praktyce nie będziesz miał z tym problemu.

Jeśli chcesz mieć 100% pewności, użycie std::memcpyzawsze ma dobrze zdefiniowane zachowanie, jeśli typ jest trywialnie kopiowalny , nawet jeśli członkowie mają nieokreśloną wartość.


Pomijając te kwestie, zawsze powinieneś zawsze poprawnie inicjować członków swojej klasy z określoną wartością w trakcie budowy, zakładając, że nie wymagasz, aby klasa miała trywialnego domyślnego konstruktora . Możesz to łatwo zrobić, używając domyślnej składni inicjalizującej członka, aby np. Zainicjować wartość członków:

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

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
orzech włoski
źródło
cóż ... ta struktura nie jest POD (zwykłe stare dane)? Oznacza to, że członkowie zostaną zainicjowani z wartościami domyślnymi? Wątpliwości
Kevin Kouketsu
Czy w tym przypadku nie jest to płytka kopia? co może pójść nie tak, chyba że dostęp do niezainicjowanego członka znajduje się w skopiowanej strukturze?
TruthSeeker
@KevinKouketsu Dodałem warunek dla przypadku, w którym wymagany jest typ trywialny / POD.
orzech
@TruthSeeker Standard mówi, że jest to niezdefiniowane zachowanie. Powód, dla którego jest to ogólnie niezdefiniowane zachowanie dla zmiennych (nieczłonkowskich), wyjaśniono w odpowiedzi AndreySemashev. Zasadniczo służy do obsługi reprezentacji pułapek za pomocą niezainicjowanej pamięci. To, czy ma to dotyczyć niejawnej konstrukcji struktur, jest kwestią związaną z kwestią CWG.
orzech
@TruthSeeker Niejawny konstruktor kopiowania jest zdefiniowany w taki sposób, aby kopiować każdy element z osobna, jakby przez bezpośrednią inicjalizację. Nie jest zdefiniowane kopiowanie reprezentacji obiektu tak, jakby przez memcpy, nawet w przypadku trywialnie kopiowalnych typów. Jedynym wyjątkiem są związki, dla których niejawny konstruktor kopiujący kopiuje reprezentację obiektu tak, jakby memcpy.
orzech
11

Zasadniczo kopiowanie niezainicjowanych danych jest niezdefiniowanym zachowaniem, ponieważ dane mogą znajdować się w stanie pułapki. Cytując stronę:

Jeśli reprezentacja obiektu nie reprezentuje żadnej wartości typu obiektu, jest znana jako reprezentacja pułapki. Uzyskiwanie dostępu do reprezentacji pułapki w jakikolwiek inny sposób niż czytanie jej poprzez wyrażenie wartości typu znaku jest zachowaniem niezdefiniowanym.

Sygnalizowanie NaN jest możliwe dla typów zmiennoprzecinkowych, a na niektórych platformach liczby całkowite mogą mieć reprezentacje pułapek.

Jednak dla trywialnie copyable rodzajów możliwe jest użycie memcpyskopiować surowego reprezentację obiektu. Jest to bezpieczne, ponieważ wartość obiektu nie jest interpretowana, a zamiast tego kopiowana jest nieprzetworzona sekwencja bajtów reprezentacji obiektu.

Andrey Semashev
źródło
Co z danymi typów, dla których wszystkie wzorce bitowe reprezentują prawidłowe wartości (np. 64-bajtowa struktura zawierająca an unsigned char[64])? Traktowanie bajtów struktury jako posiadających Nieokreślone wartości może niepotrzebnie utrudniać optymalizację, ale wymaganie od programistów ręcznego wypełniania tablicy bezużytecznymi wartościami jeszcze bardziej obniżyłoby wydajność.
supercat
Inicjowanie danych nie jest bezużyteczne, zapobiega UB, niezależnie od tego, czy jest to spowodowane reprezentacjami pułapek, czy późniejszym użyciem niezainicjowanych danych. Zerowanie 64 bajtów (1 lub 2 wiersze pamięci podręcznej) nie jest tak drogie, jak mogłoby się wydawać. A jeśli masz duże struktury, w których jest to drogie, powinieneś pomyśleć dwa razy przed ich skopiowaniem. I jestem pewien, że w pewnym momencie będziesz musiał je zainicjować.
Andrey Semashev
Operacje na kodach maszynowych, które nie mogą mieć wpływu na zachowanie programu, są bezużyteczne. Pojęcie, że za wszelką cenę należy unikać wszelkich działań określanych jako UB jako standard, mówiąc raczej, że [słowami Komitetu ds. Standardów C] UB „identyfikuje obszary możliwego rozszerzenia języka zgodnego”, jest stosunkowo nowe. Chociaż nie widziałem opublikowanego uzasadnienia dla standardu C ++, wyraźnie zrzeka się jurysdykcji nad tym, co programy C ++ są „dozwolone”, odmawiając kategoryzacji programów jako zgodnych lub niezgodnych, co oznacza, że ​​zezwala na podobne rozszerzenia.
supercat
-1

W niektórych przypadkach, takich jak opisany, Standard C ++ pozwala kompilatorom przetwarzać konstrukcje w sposób, który ich klienci uznaliby za najbardziej użyteczny, bez wymagania, aby takie zachowanie było przewidywalne. Innymi słowy, takie konstrukcje wywołują „Niezdefiniowane zachowanie”. Nie oznacza to jednak, że takie konstrukcje mają być „zabronione”, ponieważ Standard C ++ wyraźnie zrzeka się jurysdykcji w zakresie tego, co „dobrze” robią dobrze utworzone programy. Chociaż nie jestem świadomy żadnego opublikowanego dokumentu uzasadnienia standardu C ++, fakt, że opisuje on niezdefiniowane zachowanie podobnie jak C89, sugerowałoby, że zamierzone znaczenie jest podobne: „Niezdefiniowane zachowanie daje licencjodawcy implementację możliwość wychwytywania pewnych błędów programu, które są trudne zdiagnozować.

Istnieje wiele sytuacji, w których najskuteczniejszym sposobem przetworzenia czegoś jest zapisanie części struktury, o które będzie dbał dalszy kod, a pominięcie tych, o które dalszy kod nie będzie dbał. Wymaganie, aby programy inicjalizowały wszystkich członków struktury, w tym tych, o które nigdy nie będzie się troszczyć, niepotrzebnie ograniczyłoby wydajność.

Ponadto istnieją sytuacje, w których najskuteczniejsze może być zachowanie niezainicjowanych danych w sposób niedeterministyczny. Na przykład biorąc pod uwagę:

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

jeśli dalszy kod nie będzie dbał o wartości jakichkolwiek elementów x.datlub y.datktórych indeksów nie wymieniono arr, kod można zoptymalizować w celu:

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

Ta poprawa wydajności nie byłaby możliwa, gdyby programiści byli zobowiązani do jawnego napisania każdego elementu temp.dat, w tym tych, którzy nie będą się przejmować, przed skopiowaniem.

Z drugiej strony istnieją pewne aplikacje, w których ważne jest uniknięcie możliwości wycieku danych. W takich aplikacjach przydatne może być posiadanie wersji kodu, która jest instrumentem służącym do przechwytywania wszelkich prób kopiowania niezainicjowanej pamięci, bez względu na to, czy dalszy kod będzie na nią patrzeć, lub przydatne może być posiadanie gwarancji wdrożenia, że ​​dowolna pamięć których zawartość może być wyciekła, zostałaby wyzerowana lub w inny sposób nadpisana danymi niepoufnymi.

Z tego, co mogę powiedzieć, standard C ++ nie próbuje powiedzieć, że którekolwiek z tych zachowań jest na tyle bardziej przydatne niż inne, że uzasadnia to nakazanie. Jak na ironię ten brak specyfikacji może mieć na celu ułatwienie optymalizacji, ale jeśli programiści nie będą mogli wykorzystać słabych gwarancji behawioralnych, wszelkie optymalizacje zostaną zanegowane.

supercat
źródło
-2

Ponieważ wszyscy członkowie Datasą typami pierwotnymi, data2otrzymają dokładną „kopię po kawałku” wszystkich członków data. Tak więc wartość data2.bbędzie dokładnie taka sama jak wartość data.b. Nie data.bmożna jednak przewidzieć dokładnej wartości , ponieważ nie została ona wyraźnie zainicjowana. Będzie to zależeć od wartości bajtów w regionie pamięci przydzielonym dla data.

ivan.ukr
źródło
Czy możesz to wesprzeć w odniesieniu do normy? Linki dostarczone przez @walnut sugerują, że jest to niezdefiniowane zachowanie. Czy istnieje standard dla POD w standardzie?
Tomek Czajka
Mimo że poniższy link nie jest linkiem do standardu, nadal: en.cppreference.com/w/cpp/language/... "Obiekty TrivalnieCopyable można skopiować, kopiując ich reprezentacje obiektów ręcznie, np. Przy pomocy std :: memmove. Wszystkie typy danych zgodne z C język (typy POD) można w prosty sposób kopiować. ”
ivan.ukr
Jedynym „niezdefiniowanym zachowaniem” w tym przypadku jest to, że nie możemy przewidzieć wartości niezainicjowanej zmiennej składowej, ale kod się kompiluje i działa poprawnie.
ivan.ukr
1
Przytoczony przez ciebie fragment mówi o zachowaniu memmove, ale tutaj nie ma to większego znaczenia, ponieważ w moim kodzie używam konstruktora kopiowania, a nie memmove. Inne odpowiedzi sugerują, że użycie konstruktora kopiowania powoduje niezdefiniowane zachowanie. Myślę, że również źle zrozumiałeś termin „niezdefiniowane zachowanie”. Oznacza to, że język w ogóle nie daje żadnych gwarancji, np. Program może losowo ulec awarii lub uszkodzić dane lub cokolwiek zrobić. Nie oznacza to tylko, że pewna wartość jest nieprzewidywalna, byłoby to zachowanie nieokreślone.
Tomek Czajka
@ ivan.ukr Standard C ++ określa, że ​​niejawne konstruktory kopiuj / przenieś działają tak, jak w przypadku inicjacji bezpośredniej, patrz linki w mojej odpowiedzi. Dlatego konstrukcja kopiowania nie tworzy kopii „krok po kroku ”. Masz rację tylko dla typów związków, dla których kopia niejawny konstruktor jest określone, aby skopiować reprezentację obiektu jakby ręczny std::memcpy. Nic z tego nie uniemożliwia użycia std::memcpylub std::memmove. Uniemożliwia jedynie użycie niejawnego konstruktora kopii.
orzech