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;
}
c++
initialization
copy-constructor
undefined-behavior
Tomek Czajka
źródło
źródło
Odpowiedzi:
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
union
zdefiniowany , 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::memcpy
zawsze 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:
źródło
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, jakbymemcpy
.Zasadniczo kopiowanie niezainicjowanych danych jest niezdefiniowanym zachowaniem, ponieważ dane mogą znajdować się w stanie pułapki. Cytując tę stronę:
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
memcpy
skopiować surowego reprezentację obiektu. Jest to bezpieczne, ponieważ wartość obiektu nie jest interpretowana, a zamiast tego kopiowana jest nieprzetworzona sekwencja bajtów reprezentacji obiektu.źródło
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ść.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ę:
jeśli dalszy kod nie będzie dbał o wartości jakichkolwiek elementów
x.dat
luby.dat
których indeksów nie wymienionoarr
, kod można zoptymalizować w celu: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.
źródło
Ponieważ wszyscy członkowie
Data
są typami pierwotnymi,data2
otrzymają dokładną „kopię po kawałku” wszystkich członkówdata
. Tak więc wartośćdata2.b
będzie dokładnie taka sama jak wartośćdata.b
. Niedata.b
moż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 dladata
.źródło
std::memcpy
. Nic z tego nie uniemożliwia użyciastd::memcpy
lubstd::memmove
. Uniemożliwia jedynie użycie niejawnego konstruktora kopii.