Dlaczego T * może zostać przekazany do rejestru, a unikalny_ptr <T> nie może?

85

Oglądam przemówienie Chandlera Carrutha w CppCon 2019:

Brak abstrakcyjnych kosztów zerowych

podaje w nim przykład tego, jak był zaskoczony tym, ile ponosisz koszty ogólne, używając std::unique_ptr<int>ponad int*; ten segment zaczyna się mniej więcej w punkcie czasowym 17:25.

Możesz rzucić okiem na wyniki kompilacji jego przykładowej pary fragmentów (godbolt.org) - aby być świadkiem, że rzeczywiście wydaje się, że kompilator nie jest skłonny przekazać wartości unikatowej_ptr - która w rzeczywistości w dolnej linii tylko adres - w rejestrze, tylko w prostej pamięci.

Jednym z punktów, które pan Carruth przytacza około 27:00, jest to, że C ++ ABI wymaga parametrów wartości (niektórych, ale nie wszystkich; być może - typów nieprymitywnych? Typów nieskrywalnych?) Do przekazania do pamięci zamiast w rejestrze.

Moje pytania:

  1. Czy to rzeczywiście wymóg ABI na niektórych platformach? (który?) A może to tylko pesymizacja w niektórych scenariuszach?
  2. Dlaczego taki jest ABI? To znaczy, jeśli pola struktury / klasy mieszczą się w rejestrach, a nawet w jednym rejestrze - dlaczego nie mielibyśmy być w stanie przekazać go w tym rejestrze?
  3. Czy komitet normalizacyjny C ++ omawiał ten punkt w ostatnich latach, czy kiedykolwiek?

PS - Aby nie pozostawiać tego pytania bez kodu:

Zwykły wskaźnik:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}

Unikalny wskaźnik:

using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}
einpoklum
źródło
8
Nie jestem pewien, jaki jest dokładnie wymóg ABI, ale nie zakazuje on umieszczania struktur w rejestrach
Harold,
6
Gdybym miał zgadywać, powiedziałbym, że ma to związek z nietrywialnymi funkcjami składowymi wymagającymi thiswskaźnika wskazującego prawidłową lokalizację. unique_ptrma to. Rozlanie rejestru w tym celu mogłoby w pewnym sensie negować całą optymalizację „przejścia do rejestru”.
StoryTeller - Unslander Monica
2
itanium-cxx-abi.github.io/cxx-abi/abi.html#calls . To zachowanie jest wymagane. Dlaczego? itanium-cxx-abi.github.io/cxx-abi/cxx-closed.html , wyszukaj problem C-7. Istnieje pewne wyjaśnienie, ale nie jest ono zbyt szczegółowe. Ale tak, to zachowanie nie wydaje mi się logiczne. Obiekty te mogą być normalnie przekazywane przez stos. Pchanie ich do stosu, a następnie przekazywanie referencji (tylko dla „nietrywialnych” obiektów) wydaje się marnotrawstwem.
geza
6
Wygląda na to, że C ++ łamie tutaj swoje własne zasady, co jest dość smutne. Byłem w 140% przekonany, że każdy Unique_ptr po prostu zniknie po kompilacji. W końcu to tylko odroczone wywołanie destruktora, które jest znane w czasie kompilacji.
One Man Monkey Squad
7
@MaximEgorushkin: Gdybyś napisał go ręcznie, umieściłbyś wskaźnik w rejestrze, a nie na stosie.
einpoklum,

Odpowiedzi:

49
  1. Czy to rzeczywiście wymóg ABI, czy może to tylko pesymizacja w niektórych scenariuszach?

Jednym z przykładów jest dodatek binarnego interfejsu systemu V do aplikacji procesora architektury AMD64 . Ten ABI jest przeznaczony dla 64-bitowych procesorów kompatybilnych z x86 (architektura Linux x86_64). Jest on śledzony w systemach Solaris, Linux, FreeBSD, macOS, Windows Subsystem dla Linux:

Jeśli obiekt C ++ ma albo trywialny konstruktor kopii, albo trywialny destruktor, jest przekazywany przez niewidoczne odniesienie (obiekt jest zastępowany na liście parametrów wskaźnikiem, który ma klasę INTEGER).

Obiekt z nietrywialnym konstruktorem kopii lub nietrywialnym destruktorem nie może zostać przekazany przez wartość, ponieważ takie obiekty muszą mieć dobrze zdefiniowane adresy. Podobne problemy dotyczą zwracania obiektu z funkcji.

Zauważ, że tylko 2 rejestry ogólnego przeznaczenia mogą być używane do przekazywania 1 obiektu za pomocą trywialnego konstruktora kopiowania i trywialnego destruktora, tzn sizeof. W rejestrach można przekazywać tylko wartości obiektów o wartości nie większej niż 16. Zobacz Konwencje wywoływane przez Agner Fog, aby uzyskać szczegółowe informacje na temat konwencji wywoływania, w szczególności § 7.1. Przekazywanie i zwracanie przedmiotów. Istnieją odrębne konwencje wywoływania dla przekazywania typów SIMD w rejestrach.

Istnieją różne ABI dla innych architektur CPU.


  1. Dlaczego taki jest ABI? To znaczy, jeśli pola struktury / klasy mieszczą się w rejestrach, a nawet w jednym rejestrze - dlaczego nie mielibyśmy być w stanie przekazać go w tym rejestrze?

Jest to szczegół implementacji, ale gdy obsługiwany jest wyjątek, podczas odwijania stosu zniszczone obiekty z automatycznym czasem przechowywania muszą być adresowalne względem ramki stosu funkcji, ponieważ rejestry zostały do ​​tego czasu zablokowane. Kod rozwijania stosu wymaga adresów obiektów, aby wywoływać ich destruktory, ale obiekty w rejestrach nie mają adresu.

Pedantycznie niszczyciele działają na obiektach :

Obiekt zajmuje region przechowywania w okresie budowy ([class.cdtor]), przez cały okres jego życia oraz w okresie zniszczenia.

a obiekt nie może istnieć w C ++, jeśli nie jest dla niego przydzielana pamięć adresowalna , ponieważ tożsamością obiektu jest jego adres .

Gdy potrzebny jest adres obiektu z trywialnym konstruktorem kopii przechowywanym w rejestrach, kompilator może po prostu zapisać obiekt w pamięci i uzyskać adres. Jeśli konstruktor kopiowania nie jest trywialny, z drugiej strony kompilator nie może po prostu zapisać go w pamięci, musi raczej wywołać konstruktor kopiowania, który pobiera odwołanie, a zatem wymaga adresu obiektu w rejestrach. Konwencja wywoływania prawdopodobnie nie może zależeć od tego, czy konstruktor kopii został wstawiony w odbierającym, czy nie.

Innym sposobem myślenia o tym jest to, że w przypadku typów , które można łatwo kopiować, kompilator przenosi wartość obiektu w rejestrach, z którego obiekt można odzyskać w zwykłych magazynach pamięci, jeśli to konieczne. Na przykład:

void f(long*);
void g(long a) { f(&a); }

na x86_64 z systemem V ABI kompiluje się w:

g(long):                             // Argument a is in rdi.
        push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

W swoim skłaniającym do myślenia przemówieniu Chandler Carruth wspomina, że przełomowa zmiana ABI może być konieczna (między innymi) do wdrożenia niszczycielskiego ruchu, który może poprawić sytuację. IMO, zmiana ABI mogłaby być niezniszczalna, gdyby funkcje korzystające z nowego ABI jawnie wyraziły zgodę na nowe połączenie, np. Zadeklarowały je w extern "C++20" {}bloku (ewentualnie w nowej wbudowanej przestrzeni nazw do migracji istniejących interfejsów API). Aby tylko kod skompilowany na podstawie nowych deklaracji funkcji z nowym łącznikiem mógł używać nowego ABI.

Zauważ, że ABI nie ma zastosowania, gdy wywołana funkcja została wstawiona. Oprócz generowania kodu czasu łącza kompilator może wstawiać funkcje zdefiniowane w innych jednostkach tłumaczeniowych lub stosować niestandardowe konwencje wywoływania.

Maxim Egorushkin
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Samuel Liew
8

W przypadku typowych ABI nietrywialny destruktor -> nie może przekazać rejestrów

(Ilustracja punktu w odpowiedzi @ MaximEgorushkin na przykładzie @ Harolda w komentarzu; poprawiona zgodnie z komentarzem @ Yakk.)

Jeśli skompilujesz:

struct Foo { int bar; };
Foo test(Foo byval) { return byval; }

dostajesz:

test(Foo):
        mov     eax, edi
        ret

tzn. Fooobiekt jest przekazywany do testrejestru ( edi), a także zwracany do rejestru ( eax).

Gdy destruktor nie jest trywialny (jak na std::unique_ptrprzykład OP) - typowe ABI wymagają umieszczenia na stosie. Dzieje się tak nawet wtedy, gdy destruktor w ogóle nie używa adresu obiektu.

Tak więc, nawet w skrajnym przypadku destruktora, który nic nie robi, jeśli skompilujesz:

struct Foo2 {
    int bar;
    ~Foo2() {  }
};

Foo2 test(Foo2 byval) { return byval; }

dostajesz:

test(Foo2):
        mov     edx, DWORD PTR [rsi]
        mov     rax, rdi
        mov     DWORD PTR [rdi], edx
        ret

z bezużytecznym załadunkiem i przechowywaniem.

einpoklum
źródło
Ten argument mnie nie przekonuje. Nietrywialny destruktor nie robi nic, aby zakazać reguły „tak jak gdyby”. Jeśli adres nie jest przestrzegany, nie ma absolutnie żadnego powodu, dla którego taki adres musi istnieć. Tak więc zgodny kompilator mógłby z radością umieścić go w rejestrze, jeśli nie zmieni to obserwowalnego zachowania (a obecne kompilatory zrobią to, jeśli wywołujący są znani ).
ComicSansMS
1
Niestety, jest na odwrót (zgadzam się, że niektóre z nich są już nieuzasadnione). Mówiąc ściślej: nie jestem przekonany, że podane przez ciebie powody koniecznie sprawiłyby, że wszelkie możliwe ABI, które pozwoliłyby na przekazanie prądu std::unique_ptrdo rejestru niezgodne.
ComicSansMS
3
„trywialny destruktor [POTRZEBUJĄCY CITATION]” wyraźnie fałszywy; jeśli żaden kod faktycznie nie zależy od adresu, oznacza to, że adres nie musi istnieć na rzeczywistym komputerze . Adres musi istnieć w abstrakcyjnej maszyny , ale rzeczy w abstrakcyjnej maszyny, które nie mają wpływu na rzeczywiste maszyny są rzeczy, jak gdyby wolno wyeliminować.
Yakk - Adam Nevraumont
2
@einpoklum W standardzie nie ma nic, co stanowi, że rejestry istnieją. Słowo kluczowe „rejestruj” oznacza po prostu „nie możesz wziąć adresu”. Jest tylko abstrakcyjna maszyna, jeśli chodzi o standard. „tak jakby” oznacza, że ​​każda rzeczywista implementacja maszyny musi zachowywać się „tak, jakby” zachowywała się maszyna abstrakcyjna, aż do zachowania niezdefiniowanego przez standard. Obecnie istnieją bardzo trudne problemy z posiadaniem obiektu w rejestrze, o którym wszyscy mówili obszernie. Również konwencje wywoływania, których standard również nie omawia, mają praktyczne potrzeby.
Yakk - Adam Nevraumont
1
@einpoklum Nie, w tej abstrakcyjnej maszynie wszystkie rzeczy mają adresy; ale adresy są widoczne tylko w niektórych okolicznościach. Słowo registerkluczowe miało sprawić, że fizyczna maszyna będzie przechowywać coś w rejestrze, blokując rzeczy, które praktycznie utrudniają „brak adresu” na maszynie fizycznej.
Yakk - Adam Nevraumont
2

Czy to rzeczywiście wymóg ABI na niektórych platformach? (który?) A może to tylko pesymizacja w niektórych scenariuszach?

Jeśli coś jest widoczne na granicy jednostki kompilacji, to niezależnie od tego, czy jest zdefiniowane pośrednio czy jawnie, staje się częścią ABI.

Dlaczego taki jest ABI?

Podstawowym problemem jest to, że rejestry są zapisywane i przywracane przez cały czas, gdy poruszasz się w dół i w górę stosu wywołań. Nie jest więc praktyczne, aby mieć do nich odniesienie lub wskaźnik.

In-lineing i wynikające z niego optymalizacje są miłe, kiedy to się dzieje, ale projektant ABI nie może na tym polegać. Muszą zaprojektować ABI w najgorszym przypadku. Nie sądzę, żeby programiści byli bardzo zadowoleni z kompilatora, w którym ABI zmieniał się w zależności od poziomu optymalizacji.

Trywialnie kopiowalny typ może być przekazywany do rejestrów, ponieważ operację kopiowania logicznego można podzielić na dwie części. Parametry są kopiowane do rejestrów używanych do przekazywania parametrów przez dzwoniącego, a następnie kopiowane do zmiennej lokalnej przez odbiorcę. To, czy zmienna lokalna ma miejsce w pamięci, czy nie, jest zatem wyłącznie sprawą osoby odbierającej.

Typ, w którym z drugiej strony musi być użyty konstruktor kopiowania lub przenoszenia, nie może mieć takiego podziału operacji kopiowania, więc musi zostać przekazany do pamięci.

Czy komitet normalizacyjny C ++ omawiał ten punkt w ostatnich latach, czy kiedykolwiek?

Nie mam pojęcia, czy organy normalizacyjne wzięły to pod uwagę.

Oczywistym rozwiązaniem byłoby dla mnie dodanie do langauge właściwego ruchu niszczącego (zamiast obecnego domu w połowie „prawidłowego, ale poza tym nieokreślonego stanu”), a następnie wprowadzenie sposobu oznaczenia typu jako pozwalającego na „trywialne ruchy niszczące” „nawet jeśli nie pozwala na trywialne kopie.

ale takie rozwiązanie MUSI wymagać złamania ABI istniejącego kodu w celu zaimplementowania dla istniejących typów, co może przynieść spory opór (chociaż ABI pęka w wyniku nowych standardowych wersji C ++ nie jest bezprecedensowe, na przykład zmiany std :: string w C ++ 11 spowodowało przerwanie ABI ..

płyn do płukania
źródło
Czy potrafisz wyjaśnić, w jaki sposób odpowiednie destrukcyjne ruchy pozwolą na przekazanie unikalnego parametru_ptr do rejestru? Czy to dlatego, że pozwoliłoby to na zniesienie wymogu adresowalnej pamięci?
einpoklum,
Właściwe ruchy destrukcyjne umożliwiłyby wprowadzenie koncepcji trywialnych ruchów destrukcyjnych. Pozwoliłoby to na podzielenie tego trywialnego ruchu przez ABI w taki sam sposób, jak trywialne kopie mogą być dzisiaj.
płukanie
Chociaż chciałbyś również dodać regułę, że kompilator może implementować przekazywanie parametrów jako zwykły ruch lub kopię, a następnie „trywialny ruch destrukcyjny”, aby mieć pewność, że zawsze można przekazać rejestry bez względu na to, skąd pochodzi parametr.
płukanie
Ponieważ rozmiar rejestru może zawierać wskaźnik, ale strukturę Unique_ptr? Jaki jest rozmiar (Unique_ptr <T>)?
Mel Viso Martinez
@MelVisoMartinez Możesz być mylący unique_ptri shared_ptrsemantyka: shared_ptr<T>pozwala dostarczyć ctor 1) ptr x do obiektu pochodnego U, który ma zostać usunięty z typem statycznym U z wyrażeniem delete x;(więc nie potrzebujesz tutaj wirtualnego dtor) 2) lub nawet niestandardowa funkcja czyszczenia. Oznacza to, że stan środowiska wykonawczego jest używany w shared_ptrbloku kontrolnym do kodowania tych informacji. OTOH unique_ptrnie ma takiej funkcjonalności i nie koduje stanu usuwania; jedynym sposobem na dostosowanie czyszczenia jest utworzenie kolejnego instanazji szablonu (inny typ klasy).
ciekawy
-1

Najpierw musimy wrócić do tego, co oznacza przekazywanie wartości i odniesienie.

W przypadku języków takich jak Java i SML przekazywanie według wartości jest proste (i nie ma przekazywania przez odwołanie), podobnie jak kopiowanie wartości zmiennej, ponieważ wszystkie zmienne są tylko skalarami i mają wbudowane kopiowanie semantyczne: są one tym, co liczy się jako arytmetyka wpisz C ++ lub „referencje” (wskaźniki o innej nazwie i składni).

W C mamy typy skalarne i zdefiniowane przez użytkownika:

  • Skalary mają wartość liczbową lub abstrakcyjną (wskaźniki nie są liczbami, mają wartość abstrakcyjną), która jest kopiowana.
  • Typy zagregowane mają skopiowane wszystkie potencjalnie zainicjowane elementy:
    • dla typów produktów (tablic i struktur): rekurencyjnie kopiowane są wszystkie elementy struktur i elementów tablic (składnia funkcji C nie umożliwia bezpośredniego przekazywania tablic według wartości, tylko elementy tablicy struktury, ale to szczegół ).
    • dla typów sum (związków): wartość „aktywnego członka” zostaje zachowana; oczywiście, członek po kopii członka nie jest w porządku, ponieważ nie wszyscy członkowie mogą być inicjowani.

W C ++ typy zdefiniowane przez użytkownika mogą mieć zdefiniowane przez użytkownika semantyczne kopie, które umożliwiają prawdziwie „obiektowe” programowanie z obiektami posiadającymi ich zasoby i operacje „głębokiego kopiowania”. W takim przypadku operacja kopiowania jest tak naprawdę wywołaniem funkcji, która może prawie wykonywać dowolne operacje.

W przypadku struktur C skompilowanych jako C ++ „kopiowanie” jest nadal definiowane jako wywoływanie operacji kopiowania zdefiniowanej przez użytkownika (konstruktora lub operatora przypisania), które są domyślnie generowane przez kompilator. Oznacza to, że semantyka wspólnego programu podzbiorów C / C ++ jest różna w C i C ++: w C kopiowany jest cały typ agregatu, w C ++ wywoływana jest domyślnie funkcja kopiowania w celu skopiowania każdego elementu; końcowy wynik jest taki, że w każdym przypadku każdy członek jest kopiowany.

(Sądzę, że istnieje wyjątek, gdy kopiowana jest struktura wewnątrz związku).

Zatem dla typu klasy jedynym sposobem (poza kopiami unii) na utworzenie nowej instancji jest użycie konstruktora (nawet dla tych z trywialnymi konstruktorami generowanymi przez kompilator).

Nie możesz pobrać adresu wartości za pośrednictwem jednoargumentowego operatora, &ale to nie znaczy, że nie ma obiektu wartości; a obiekt z definicji ma adres ; a ten adres jest nawet reprezentowany przez konstrukcję składni: obiekt typu klasy może być utworzony tylko przez konstruktor i ma thiswskaźnik; ale dla trywialnych typów nie ma konstruktora napisanego przez użytkownika, więc nie ma miejsca na umieszczeniethis dopóki kopia nie zostanie zbudowana i nazwana.

Dla typu skalarnego wartością obiektu jest wartość obiektu, czysta wartość matematyczna przechowywana w obiekcie.

W przypadku typu klasy jedynym pojęciem wartości obiektu jest kolejna kopia obiektu, którą może wykonać tylko konstruktor kopii, prawdziwa funkcja (chociaż w przypadku typów trywialnych funkcja ta jest tak trywialna, że ​​czasami może być utworzony bez wywoływania konstruktora). Oznacza to, że wartość obiektu jest wynikiem zmiany stanu programu globalnego przez wykonanie . Nie ma dostępu matematycznego.

Więc przekazanie przez wartość tak naprawdę nie jest rzeczą: przekazanie przez wywołanie konstruktora kopiowania , które jest mniej ładne. Oczekuje się, że konstruktor kopiowania wykona rozsądną operację „kopiowania” zgodnie z odpowiednią semantią typu obiektu, z poszanowaniem jego wewnętrznych niezmienników (które są abstrakcyjnymi właściwościami użytkownika, a nie wewnętrznymi właściwościami C ++).

Przekazywanie przez wartość obiektu klasy oznacza:

  • utwórz inną instancję
  • następnie wywołaj funkcję działającą w tej instancji.

Zauważ, że problem nie ma nic wspólnego z tym, czy sama kopia jest obiektem o adresie: wszystkie parametry funkcji są obiektami i mają adres (na poziomie semantycznym języka).

Problem polega na tym, czy:

  • kopia jest nowym obiektem zainicjowanym czystą wartością matematyczną (prawdziwa czysta wartość) oryginalnego obiektu, podobnie jak skalary;
  • lub kopia jest wartością oryginalnego obiektu , jak w przypadku klas.

W przypadku trywialnego typu klasy nadal możesz zdefiniować element członkowski kopii oryginału, dzięki czemu możesz zdefiniować czystą wartość oryginału z powodu trywialności operacji kopiowania (konstruktor kopii i przypisanie). Nie jest tak w przypadku dowolnych specjalnych funkcji użytkownika: wartością oryginału musi być skonstruowana kopia.

Obiekt klasy musi zbudować obiekt wywołujący; konstruktor formalnie ma thiswskaźnik, ale formalizm nie ma tutaj znaczenia: wszystkie obiekty formalnie mają adres, ale tylko te, które faktycznie używają swojego adresu w sposób nie tylko lokalny (w przeciwieństwie *&i = 1;do tego, w jaki sposób adresowanie jest wyłącznie lokalne), muszą mieć dobrze zdefiniowane adres.

Obiekt musi być absolutnie przekazany przez adres, jeśli wydaje się, że ma adres w obu tych osobno skompilowanych funkcjach:

void callee(int &i) {
  something(&i);
}

void caller() {
  int i;
  callee(i);
  something(&i);
}

Tutaj, nawet jeśli something(address)jest to czysta funkcja lub makro lub cokolwiek (jak printf("%p",arg)), które nie może przechowywać adresu lub komunikować się z innym bytem, ​​musimy przekazać adres, ponieważ adres musi być dobrze zdefiniowany dla unikalnego obiektuint który ma unikalny obiekt tożsamość.

Nie wiemy, czy funkcja zewnętrzna będzie „czysta” pod względem przekazywanych do niej adresów.

W tym przypadku możliwość rzeczywistego wykorzystania adresu zarówno w trywialnym konstruktorze, jak i destruktorze po stronie dzwoniącego jest prawdopodobnie powodem podjęcia bezpiecznej, uproszczonej trasy i nadania obiektowi tożsamości w dzwoniącym i przekazania jego adresu, ponieważ powoduje upewnij się, że każde nietrywialne użycie jego adresu w konstruktorze, po budowie i w destruktorze jest spójne : thismusi wydawać się takie samo w całym istnieniu obiektu.

Nietrywialny konstruktor lub niszczyciel, jak każda inna funkcja, może używać thiswskaźnika w sposób, który wymaga spójności jego wartości, nawet jeśli niektóre obiekty z nieistotnymi funkcjami mogą nie:

struct file_handler { // don't use that class!
    file_handler () { this->fileno = -1; }
    file_handler (int f) { this->fileno = f; }
    file_handler (const file_handler& rhs) {
        if (this->fileno != -1)
            this->fileno = dup(rhs.fileno);
        else
            this->fileno = -1;
    }
    ~file_handler () {
        if (this->fileno != -1)
            close(this->fileno); 
    }
    file_handler &operator= (const file_handler& rhs);
};

Zauważ, że w takim przypadku, pomimo jawnego użycia wskaźnika (jawnej składni this->), tożsamość obiektu jest nieistotna: kompilator mógłby dobrze użyć bitowego kopiowania obiektu, aby go przenieść i wykonać „kopiowanie elision”. Jest to oparte na poziomie „czystości” użycia thisw specjalnych funkcjach członkowskich (adres nie ucieka).

Ale czystość nie jest atrybutem dostępnym na standardowym poziomie deklaracji (istnieją rozszerzenia kompilatora, które dodają opis czystości w deklaracji funkcji niewbudowanej), więc nie można zdefiniować ABI na podstawie czystości kodu, który może nie być dostępny (kod może lub mogą nie być wbudowane i dostępne do analizy).

Czystość mierzy się jako „z pewnością czystą” lub „nieczystą lub nieznaną”. Wspólna podstawa, czyli górna granica semantyki (właściwie maksimum), lub LCM (najmniejsza wspólna wielokrotność) jest „nieznana”. Więc ABI decyduje się na nieznane.

Podsumowanie:

  • Niektóre konstrukcje wymagają kompilatora do zdefiniowania tożsamości obiektu.
  • Wskaźnik ABI jest zdefiniowany w kategoriach klas programów, a nie konkretnych przypadków, które można zoptymalizować.

Możliwe przyszłe prace:

Czy adnotacja czystości jest wystarczająca do uogólnienia i wystandaryzowania?

ciekawy
źródło
1
Twój pierwszy przykład wydaje się mylący. Myślę, że ogólnie masz rację, ale na początku myślałem, że robisz analogię do kodu w pytaniu. Ale void foo(unique_ptr<int> ptr)przyjmuje obiekt klasy według wartości . Ten obiekt ma element wskaźnikowy, ale mówimy o tym, że sam obiekt klasy jest przekazywany przez referencję. (Ponieważ nie jest to trywialne kopiowanie, więc jego konstruktor / destruktor potrzebuje spójności this.) To jest prawdziwy argument i nie jest powiązany z pierwszym przykładem jawnego przekazania przez referencję ; w takim przypadku wskaźnik jest przekazywany do rejestru.
Peter Cordes
@PeterCordes „ robiłeś analogię do kodu w pytaniu. ” Zrobiłem dokładnie to. „ obiekt klasy według wartości ” Tak. Prawdopodobnie powinienem wyjaśnić, że generalnie nie ma czegoś takiego jak „wartość” obiektu klasy, więc wartość dla typu innego niż matematyczne nie jest „wartością”. „ Ten obiekt ma element wskaźnikowy ”. Charakter ptr podobny do „inteligentnego ptr” jest nieistotny; podobnie jak członek ptr „inteligentnego ptr”. Ptr jest po prostu skalarem jak int: napisałem przykład „inteligentnego fileno”, który pokazuje, że „własność” nie ma nic wspólnego z „przenoszeniem ptr”.
ciekawy
1
Wartością obiektu klasy jest jego reprezentacja obiektu. Na unique_ptr<T*>ten ma taki sam rozmiar i układ jak T*i wpisuje się w rejestrze. Obiekty klasy, które można kopiować, mogą być przekazywane przez wartości do rejestrów w systemie x86-64 System V, podobnie jak większość konwencji wywoływania. To sprawia, że kopię tego unique_ptrobiektu, w przeciwieństwie do swojej intprzykład gdy wywoływany użytkownika &i jest adres osoby dzwoniącej i, ponieważ przekazywane przez referencję na poziomie C ++ , a nie tylko jako szczegółach realizacji ASM.
Peter Cordes
1
Err, poprawka do mojego ostatniego komentarza. To nie tylko tworzenia kopii tego unique_ptrobiektu; używa, std::movewięc można go bezpiecznie skopiować, ponieważ nie da to 2 kopii tego samego unique_ptr. Ale dla typu, który można w prosty sposób skopiować, tak, kopiuje on cały obiekt agregujący. Jeśli jest to pojedynczy element członkowski, dobre konwencje wywoływania traktują to tak samo jak skalar tego typu.
Peter Cordes,
1
Wygląda lepiej. Uwagi: W przypadku struktur C skompilowanych jako C ++ - Nie jest to przydatny sposób wprowadzenia różnicy między C ++. W C ++ struct{}jest strukturą C ++. Być może powinieneś powiedzieć „zwykłe struktury” lub „w przeciwieństwie do C”. Ponieważ tak, jest różnica. Jeśli użyjesz atomic_intjako członka struktury, C skopiuje go nieatomowo, błąd C ++ na usuniętym konstruktorze kopiowania. Zapominam, co C ++ robi na strukturach z volatileczłonkami. C pozwoli ci struct tmp = volatile_struct;skopiować całą rzecz (przydatne dla SeqLock); C ++ nie.
Peter Cordes,