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 this
wskaź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 this
wskaź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 : this
musi wydawać się takie samo w całym istnieniu obiektu.
Nietrywialny konstruktor lub niszczyciel, jak każda inna funkcja, może używać this
wskaź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 this
w 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?