Interesującą cechą języka C w porównaniu z innymi językami jest to, że wiele jego typów danych opiera się na wielkości słowa architektury docelowej, a nie jest określanych w kategoriach bezwzględnych. Chociaż pozwala to na użycie języka do pisania kodu na maszynach, które mogą mieć trudności z niektórymi typami, bardzo utrudnia projektowanie kodu, który będzie działał konsekwentnie na różnych architekturach. Rozważ kod:
uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;
W architekturze, w której int
jest 16 bitów (nadal tak jest w przypadku wielu małych mikrokontrolerów), kod ten przypisałby wartość 1 przy użyciu dobrze zdefiniowanego zachowania. Na komputerach, na których int
jest 64 bity, przypisałby wartość 4294836225, ponownie używając dobrze zdefiniowanego zachowania. Na komputerach, na których int
jest 32 bity, prawdopodobnie przypisałby wartość -131071 (nie wiem, czy byłoby to zachowanie zdefiniowane w implementacji czy niezdefiniowane). Mimo że kod nie używa niczego poza tymi, które nominalnie mają być „stałymi” typami, standard wymagałby, aby dwa różne rodzaje kompilatorów używanych obecnie dały dwa różne wyniki, a wiele popularnych kompilatorów dzisiaj dałoby trzeci.
Ten konkretny przykład jest nieco wymyślony, ponieważ nie spodziewałbym się, że w kodzie rzeczywistym przypisze produkt dwóch 16-bitowych wartości bezpośrednio do wartości 64-bitowej, ale został wybrany jako krótki przykład pokazujący trzy sposoby liczb całkowitych promocje mogą wchodzić w interakcje z niepodpisanymi typami o podobnej wielkości. Istnieją pewne sytuacje w świecie rzeczywistym, w których matematyka na niepodpisanych typach musi być wykonywana zgodnie z regułami arytmetyki liczb całkowitych matematycznych, inne, w których konieczne jest, aby były wykonywane zgodnie z regułami arytmetyki modułowej, i niektóre, w których tak naprawdę nie jest ” to ważne. Wiele rzeczywistych kodów, takich jak sumy kontrolne, polega na uint32_t
zawijaniu arytmetycznym mod 2³² i na możliwości wykonywania dowolnych zadańuint16_t
arytmetyka i otrzymuj wyniki, które są, co najmniej, zdefiniowane jako dokładne mod 65536 (w przeciwieństwie do wyzwalania Nieokreślonego Zachowania).
Chociaż ta sytuacja wydaje się wyraźnie niepożądana (i stanie się bardziej, gdy 64-bitowe przetwarzanie stanie się normą dla wielu celów), komitet normalizacyjny C z tego, co zaobserwowałem, woli wprowadzić funkcje językowe, które są już używane w niektórych znaczących produkcjach środowiska, zamiast wymyślać je „od zera”. Czy istnieją jakieś znaczące rozszerzenia języka C, które pozwoliłyby kodowi określić nie tylko sposób przechowywania typu, ale także sposób jego zachowania w scenariuszach z możliwymi promocjami? Widzę co najmniej trzy sposoby, w jakie rozszerzenie kompilatora może rozwiązać takie problemy:
Dodając dyrektywę, która instruuje kompilator, aby wymusił określone „podstawowe” typy liczb całkowitych na określone rozmiary.
Dodając dyrektywę, która instruuje kompilator, aby oceniał różne scenariusze promocji, tak jakby typy komputerów miały określone rozmiary, niezależnie od rzeczywistych rozmiarów typów w architekturze docelowej.
Umożliwiając za pomocą deklarowania typów o określonych cechach (np. Deklarując, że typ powinien zachowywać się jak pierścień algebraiczny zwijający mod-65536, niezależnie od rozmiaru słowa podstawowego, i nie powinien być domyślnie konwertowany na inne typy; dodanie a
wrap32
doint
powinno dać wynik typu,wrap32
niezależnie od tego, czyint
jest większy niż 16 bitów, podczas gdy dodawaniewrap32
bezpośrednio do awrap16
powinno być nielegalne (ponieważ żaden z nich nie może przekonwertować na inny).
Moja własna preferencja byłaby trzecią alternatywą, ponieważ pozwoliłaby nawet maszynom o nietypowych rozmiarach słów pracować z dużą ilością kodu, który oczekuje, że zmienne „zawiną” się tak, jak w przypadku potęgi dwóch rozmiarów; kompilator może wymagać dodania instrukcji maskowania bitów, aby typ zachowywał się odpowiednio, ale jeśli kod potrzebuje typu, który otacza mod 65536, lepiej jest, aby kompilator generował takie maskowanie na komputerach, które go potrzebują, niż zaśmiecać nim kod źródłowy lub po prostu mieć taki kod przez nieużywalny na komputerach, na których takie maskowanie byłoby potrzebne. Zastanawiam się jednak, czy istnieją jakieś powszechne rozszerzenia, które osiągnęłyby przenośne zachowanie za pomocą dowolnego z powyższych środków, lub za pomocą środków, o których nie myślałem.
Aby wyjaśnić, czego szukam, jest kilka rzeczy; w szczególności:
Chociaż istnieje wiele sposobów pisania kodu, aby zapewnić pożądaną semantykę (np. Definiowanie makr w celu wykonania obliczeń matematycznych na operandach niepodpisanych o określonej wielkości, aby uzyskać wynik, który jawnie zawija się lub nie) lub przynajmniej zapobiega niepożądanym semantyka (np. warunkowo zdefiniuj typ,
wrap32_t
który ma znaleźć sięuint32_t
w kompilatorach, w których auint32_t
nie zostałby awansowany, i stwierdź, że lepiej jest dla kodu, który wymagawrap32_t
niepowodzenia kompilacji na komputerach, na których ten typ zostałby awansowany, niż uruchomić go i dać fałszywe zachowanie), jeśli istnieje jakikolwiek sposób na napisanie kodu, który grałby najkorzystniej w przyszłych rozszerzeniach języka, użycie tego byłoby lepsze niż opracowanie własnego podejścia.Mam kilka solidnych pomysłów na to, jak można rozszerzyć język, aby rozwiązać wiele problemów z liczbami całkowitymi, umożliwiając kodowi uzyskanie identycznej semantyki na maszynach o różnych rozmiarach słów, ale zanim poświęcę znaczący czas na ich pisanie, chciałbym wiedzieć, jakie wysiłki w tym kierunku zostały już podjęte.
W żaden sposób nie chcę być postrzegany jako dyskredytujący Komitet ds. Norm C lub pracę, którą wykonali; Oczekuję jednak, że w ciągu kilku lat konieczne będzie poprawne działanie kodu na komputerach, na których „naturalny” typ promocyjny miałby 32 bity, a także na tych, na których byłby to 64 bity. Myślę, że przy niektórych skromnych rozszerzeniach języka (bardziej skromnych niż wiele innych zmian między C99 i C14) możliwe byłoby nie tylko zapewnienie czystego sposobu wydajnego korzystania z architektur 64-bitowych, ale także okazja ułatwienia interakcji z maszyny o „nietypowym rozmiarze słowa”, które normalnie od dawna stawiały na głowie, aby obsługiwać [np. umożliwiając maszynom z 12-bitowym char
kodem uruchomienie, które oczekujeuint32_t
owinąć mod 2³²]. W zależności od kierunku, w którym podążą przyszłe rozszerzenia, oczekiwałbym również, że powinna istnieć możliwość zdefiniowania makr, które pozwoliłyby na użytkowanie dzisiejszego kodu w dzisiejszych kompilatorach, w których domyślne typy liczb całkowitych zachowują się jak „oczekiwane”, ale także w przyszłych kompilatorach, w których liczba całkowita typy będą domyślnie zachowywać się inaczej, ale gdzie mogą zapewnić wymagane zachowania.
int
, ale wciąż się wślizguje. (Ponownie zakładając, że rozumiem standard C jest poprawny.)
int
jest większa niżuint16_t
, operandy mnożenia będą promowane do,int
a mnożenie zostanie przeprowadzone jakoint
mnożenie, a wynikowaint
wartość zostanie przekonwertowanaint64_t
na inicjalizacjęwho_knows
.