Co zapobiega nakładaniu się sąsiednich członków na zajęcia?

12

Rozważ następujące trzy structs:

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

Na typowych platformach, na których intsą 4 bajty, rozmiary, wyrównania i całkowite wypełnienie 1 są następujące:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

Przechowywanie elementów blubi blobelementów nie zachodzi na siebie , mimo że rozmiar 1 blobmógłby w zasadzie „pasować” do wypełnienia blub.

C ++ 20 wprowadza no_unique_addressatrybut, który pozwala sąsiednim pustym członkom na dzielenie tego samego adresu. Pozwala to również wyraźnie na opisany powyżej scenariusz użycia wypełnienia jednego elementu do przechowywania innego. Z preferencji (moje podkreślenie):

Wskazuje, że ten członek danych nie musi mieć adresu odrębnego od wszystkich innych niestatycznych członków danych swojej klasy. Oznacza to, że jeśli element członkowski ma pusty typ (np. Bezstanowy alokator), kompilator może go zoptymalizować, aby nie zajmował miejsca, tak jakby był pustą bazą. Jeśli element członkowski nie jest pusty, wszelkie wypełnienia ogona mogą zostać ponownie wykorzystane do przechowywania innych elementów danych.

Rzeczywiście, jeśli użyjemy tego atrybutu blub b0, wielkość blakropli spadnie do 8, więc blobrzeczywiście jest on przechowywany w blub postaci widocznej na godbolt .

Wreszcie dochodzimy do mojego pytania:

Jaki tekst w standardach (od C ++ 11 do C ++ 20) zapobiega temu nakładaniu się no_unique_address, w przypadku obiektów, których nie można w prosty sposób skopiować?

Muszę wykluczyć z powyższego obiekty trywialnie kopiowalne (TC), ponieważ w przypadku obiektów TC dozwolone jest przechodzenie std::memcpyz jednego obiektu do drugiego, w tym podobiektów składowych, a jeśli magazyn byłby nałożony, to by się zepsuło (ponieważ całość lub część magazynu ponieważ sąsiedni członek zostanie zastąpiony) 2 .


1 Obliczamy wypełnienie po prostu rekurencyjnie jako różnicę między rozmiarem struktury a rozmiarem wszystkich jej elementów składowych.

2 Jest to dlaczego mam konstruktory kopia zdefiniowane: zrobić blubi blobnie trywialnie copyable .

BeeOnRope
źródło
Nie badałem tego, ale zgaduję zasadę „jak gdyby”. Jeśli nie ma zauważalnej różnicy (termin o bardzo specyficznym znaczeniu btw) w stosunku do maszyny abstrakcyjnej (z którą kompilowany jest twój kod), to kompilator może zmienić kod, jak chce.
Jesper Juhl
Całkiem pewne, że to duplikat tego: stackoverflow.com/questions/53837373/…
NathanOliver
@JesperJuhl - racja, ale pytam, dlaczego nie , nie dlaczego , a zasada „jak gdyby” zwykle dotyczy pierwszej, ale nie ma sensu w drugiej. Ponadto „jakby jakby” nie jest jasne w przypadku układu struktury, który jest zwykle kwestią globalną, a nie lokalną. Ostatecznie kompilator musi mieć jeden spójny zestaw reguł dotyczących układu, z wyjątkiem struktur, które mogą nigdy nie „uciec”.
BeeOnRope
1
@BeeOnRope Niestety nie mogę odpowiedzieć na twoje pytanie. Dlatego właśnie opublikowałem komentarz, a nie odpowiedź. To, co dostałeś w tym komentarzu, było moim najlepszym domysłem w kierunku wyjaśnienia, ale nie znam odpowiedzi (ciekawej, gdybym sam się tego nauczył - dlatego uzyskałeś poparcie).
Jesper Juhl
1
@NicolBolas - czy odpowiadasz na właściwe pytanie? Tu nie chodzi o wykrywanie bezpiecznych kopii lub czegokolwiek innego. Ciekawe, dlaczego padding nie może być ponownie użyty między członkami. W każdym razie się mylisz: trywialne kopiowanie jest własnością tego typu i zawsze tak było. Jednak, aby bezpiecznie skopiować obiekt, musi on mieć zarówno typ TC (właściwość typu), jak i nie być potencjalnie pokrywającym się podmiotem (właściwość obiektu, która, jak sądzę, jest miejscem, w którym się pomyliłeś). Nadal nie wiem, dlaczego mówimy tutaj o kopiach.
BeeOnRope

Odpowiedzi:

1

Norma jest strasznie cicha, gdy mówi się o modelu pamięci, i nie bardzo wyraźnie mówi o niektórych terminach, których używa. Ale myślę, że znalazłem działającą argumentację (która może być nieco słaba)

Najpierw dowiedzmy się, co jest nawet częścią obiektu. [basic.types] / 4 :

Obiektowa reprezentacja obiektu typu Tjest sekwencją N unsigned charobiektów zajmowanych przez obiekt typu T, gdzie Njest równa sizeof(T). Reprezentacja wartości obiektu typu Tjest zbiorem bitów uczestniczących w reprezentowaniu wartości typu T. Bity w reprezentacji obiektu, które nie są częścią reprezentacji wartości, są bitami wypełniającymi.

Reprezentacja obiektowa b0składa się z sizeof(blub) unsigned charobiektów, czyli 8 bajtów. Bity wypełniające są częścią obiektu.

Żaden obiekt nie może zajmować przestrzeni innego obiektu, jeśli nie jest w nim zagnieżdżony [basic.life] /1.5 :

Żywotność obiektu otypu Tkończy się, gdy:

[...]

(1.5) pamięć ozajmowana przez obiekt jest zwalniana lub jest ponownie wykorzystywana przez obiekt, który nie jest zagnieżdżony w ([intro.object]).

Tak więc czas życia b0dobiegałby końca, gdy zajmowana przez nią pamięć byłaby ponownie wykorzystywana przez inny obiekt, tj b1. Nie sprawdziłem tego, ale myślę, że standardowy nakazuje, aby podobiektyw obiektu, który jest żywy, również powinien być żywy (i nie mogłem sobie wyobrazić, jak to powinno działać inaczej).

Więc przechowywania które b0 okupuje nie mogą być wykorzystywane przez b1. W standardzie nie znalazłem definicji „okup”, ale myślę, że rozsądną interpretacją byłaby „część reprezentacji obiektu”. W opisie obiektu opisującego cytat używane są słowa „podejmować” 1 . W tym przypadku byłoby to 8 bajtów, więc blapotrzeba co najmniej jeszcze jednego b1.

Szczególnie w przypadku podobiektów (a więc między innymi elementów danych niestatycznych) istnieje również warunek [intro.object] / 9 (ale zostało to dodane w C ++ 20, thx @BeeOnRope)

Dwa obiekty z nakładającymi się okresami istnienia, które nie są polami bitowymi, mogą mieć ten sam adres, jeśli jeden jest zagnieżdżony w drugim lub jeśli co najmniej jeden jest podobiektem o zerowej wielkości i są one różnego rodzaju; w przeciwnym razie mają odrębne adresy i zajmują rozłączne bajty pamięci .

(moje podkreślenie) Tutaj znowu mamy problem, że „zajmuje” nie jest zdefiniowane i ponownie argumentowałbym, aby wziąć bajty w reprezentacji obiektu. Zwróć uwagę, że do tego [basic.memobj] / przypisu 29 znajduje się przypis

Zgodnie z zasadą „jak gdyby” implementacja może przechowywać dwa obiekty pod tym samym adresem maszyny lub nie przechowywać obiektu w ogóle, jeśli program nie może zaobserwować różnicy ([wykonanie wstępne]).

Co może pozwolić kompilatorowi na przełamanie tego, jeśli może udowodnić, że nie ma zauważalnego efektu ubocznego. Sądzę, że jest to dość skomplikowane w przypadku tak fundamentalnej rzeczy, jak układ obiektu. Być może dlatego ta optymalizacja jest podejmowana tylko wtedy, gdy użytkownik dostarczy informację, że nie ma powodu, aby mieć rozłączne obiekty poprzez dodanie [no_unique_address]atrybutu.

tl; dr: Wypełnienie może być częścią obiektu, a członkowie muszą być rozłączni.


1 Nie mogłem się oprzeć dodaniu odnośnika, który może zajmować: Webster's Revised Unabridged Dictionary, G. i C. Merriam, 1913 (wyróżnienie moje)

  1. Aby zachować lub wypełnić wymiary; zająć pokój lub przestrzeń; zakryć lub wypełnić; ponieważ obóz zajmuje pięć akrów ziemi. Sir J. Herschel.

Jakie standardowe indeksowanie byłoby kompletne bez indeksowania w słowniku?

n314159
źródło
2
Wydaje mi się, że część „zajmuj rozłączne bajty pamięci” z pliku do. Magazynowanie byłaby dla mnie wystarczająca - ale to sformułowanie zostało dodane w C ++ 20 tylko w ramach dodanej zmiany no_unique_address. Pozostawia sytuację przed C ++ 20 mniej jasną. Nie zrozumiałem twojego rozumowania prowadzącego do „Żaden obiekt nie może zajmować przestrzeni innego, jeśli nie jest w nim zagnieżdżony” z basic.life/1.5, w szczególności jak uzyskać z „miejsca, które zajmuje obiekt, jest zwolnione” na „żaden obiekt nie może zajmować przestrzeni innego”.
BeeOnRope
1
Dodałem małe wyjaśnienie do tego ustępu. Mam nadzieję, że dzięki temu będzie bardziej zrozumiały. W przeciwnym razie przyjrzę się temu jutro, teraz jest już dla mnie dość późno.
n314159
„Dwa obiekty z nakładającymi się okresami istnienia, które nie są polami bitowymi, mogą mieć ten sam adres, jeśli jeden jest zagnieżdżony w drugim lub jeśli co najmniej jeden jest podobiektem o zerowej wielkości i są one różnego typu” 2 obiekty z nakładającymi się okresami istnienia tego samego typu, mają ten sam adres .
Prawnik językowy
Przepraszam, mógłbyś opracować? Cytujesz standardowy cytat z mojej odpowiedzi i podajesz przykład, który jest z tym nieco sprzeczny. Nie jestem pewien, czy jest to komentarz do mojej odpowiedzi i czy właśnie to powinien mi powiedzieć. Jeśli chodzi o twój przykład, powiedziałbym, że należy wziąć pod uwagę jeszcze inne części standardu (jest paragraf o niepodpisanej tablicy char zapewniającej pamięć dla innego obiektu, coś na temat optymalizacji bazy zerowej i jeszcze dalej należy sprawdzić, czy nowe umieszczenie ma specjalne dodatki, wszystkie rzeczy, które nie uważam za istotne w przykładzie PO)
n314159
@ n314159 Myślę, że to sformułowanie może być wadliwe.
Język Lawyer