(Я думаю, этот вопрос может относиться ко многим типизированным языкам, но я решил использовать C ++ в качестве примера.)
Почему нет возможности просто написать:
struct foo {
little int x; // little-endian
big long int y; // big-endian
short z; // native endianness
};
указать порядковый номер для конкретных членов, переменных и параметров?
Я понимаю, что тип переменной не только определяет, сколько байтов используется для хранения значения, но также и как эти байты интерпретируются при выполнении вычислений.
Например, каждое из этих двух объявлений выделяет один байт, и для обоих байтов каждая возможная 8-битная последовательность является допустимым значением:
signed char s;
unsigned char u;
но одна и та же двоичная последовательность может интерпретироваться по-разному, например, 11111111
будет означать -1 при назначении s
но 255 при назначении на u
, Когда в одном и том же вычислении участвуют переменные со знаком и без знака, компилятор (в основном) заботится о правильных преобразованиях.
В моем понимании, порядковый номер — это всего лишь вариация одного и того же принципа: другая интерпретация двоичного шаблона, основанная на информации времени компиляции о памяти, в которой он будет храниться.
Кажется очевидным иметь такую возможность в типизированном языке, которая позволяет программировать на низком уровне. Тем не менее, это не часть C, C ++ или любого другого языка, который я знаю, и я не нашел никакого обсуждения по этому поводу в Интернете.
Я постараюсь обобщить некоторые выводы из множества комментариев, которые я получил в первый час после того, как спросил:
Кроме того, теперь я понимаю, что подписанность и порядковый номер не являются идеальной аналогией, потому что:
big int
а также little int
будет иметь точно такой же диапазон значений.unsigned char
и (при условии, что char
имеет 8 бит) 130 не может быть представлен signed char
,Так что изменение порядкового номера некоторых переменных никогда не изменит поведение программы (за исключением побайтного доступа), тогда как изменение подписи обычно будет.
[intro.abstract]/1
:Семантические описания в этом документе определяют параметризованную недетерминированную абстрактную машину.
Этот документ не предъявляет никаких требований к структуре соответствующих реализаций.
В частности, им не нужно копировать или эмулировать структуру абстрактной машины.
Скорее, соответствующие реализации требуются для эмуляции (только) наблюдаемого поведения абстрактной машины, как объяснено ниже.
C ++ не может определить спецификатор порядка байтов, так как он не имеет понятия байтов.
О разнице между знаменательностью и порядком байтов ОП писал
В моем понимании, endianness — это всего лишь вариация одного и того же принципа [(signness)]: другая интерпретация двоичного шаблона, основанная на информации времени компиляции о памяти, в которой он будет храниться.
Я бы поспорил, что Signness имеет смысл и репрезентативный аспект.1. Какие [intro.abstract]/1
подразумевается, что C ++ заботится только о семантический, и никогда не обращается к тому, как подписанное число должно быть представлено в памяти2. На самом деле, «знак бит» появляется только один раз в спецификации C ++ и обратитесь к определенному реализацией значению.
С другой стороны, у байтов есть только репрезентативный аспект: порядок байтов не имеет смысла.
С С ++ 20, std::endian
появляется. Это все еще определяется реализацией, но давайте проверим порядковый номер узла, не завися от старые трюки, основанные на неопределенном поведении.
1) Семантический аспект: целое число со знаком может представлять значения ниже нуля; репрезентативный аспект: необходимо, например, зарезервировать немного, чтобы передать положительный / отрицательный знак.
2) В том же духе, C ++ никогда не описывает, как должно быть представлено число с плавающей запятой, часто используется IEEE-754, но это выбор, сделанный реализацией, в любом случае, обеспечиваемой стандартом: [basic.fundamental]/8
«Представление значений типов с плавающей точкой определяется реализацией».
В дополнение к ответу YSC, давайте возьмем ваш пример кода и рассмотрим, к чему он может стремиться
struct foo {
little int x; // little-endian
big long int y; // big-endian
short z; // native endianness
};
Вы можете надеяться, что это точно определит макет для независимого от архитектуры обмена данными (файл, сеть, что угодно)
Но это не может сработать, потому что некоторые вещи все еще не определены:
little int32_t
, big int64_t
а также int16_t
соответственно если это то что ты хочешь#pragma
или же __attribute__((packed))
или какое-то другое специфичное для компилятора расширениеВ качестве альтернативы, вы можете просто захотеть отразить последовательность некоторых указанных аппаратных средств — но big
а также little
не охватывайте все возможности здесь (только два наиболее распространенных).
Таким образом, предложение является неполным (оно не отличает все разумные механизмы упорядочения байтов), неэффективно (оно не достигает того, к чему оно стремится) и имеет дополнительные недостатки:
Спектакль
Изменение порядкового номера переменной из собственного порядка байтов должно либо отключить арифметику, сравнения и т. Д. (Так как аппаратное обеспечение не могу правильно выполнить их в этом типе), или нужно молча добавить больше кода, создавая временные последовательности для работы над ними.
Аргумент здесь не в том, что ручное преобразование в / из собственного порядка байтов Быстрее, Дело в том, что его явное управление упрощает минимизацию количества ненужных преобразований и значительно упрощает рассуждения о том, как будет вести себя код, чем если бы преобразования были неявными.
сложность
Все, перегруженное или специализированное для целочисленных типов, теперь нуждается в вдвое большем количестве версий, чтобы справиться с редким случаем, когда ему передается значение не-native-endianness. Даже если это просто оболочка для пересылки (с парой приведений для перевода в / из собственного порядка), это все равно много кода без какой-либо заметной выгоды.
Последний аргумент против изменения языка для поддержки этого заключается в том, что вы можете легко сделать это в коде. Изменение синтаксиса языка является серьезной проблемой и не дает никаких очевидных преимуществ по сравнению с чем-то вроде оболочки типа:
// store T with reversed byte order
template <typename T>
class Reversed {
T val_;
static T reverse(T); // platform-specific implementation
public:
explicit Reversed(T t) : val_(reverse(t)) {}
Reversed(Reversed const &other) : val_(other.val_) {}
// assignment, move, arithmetic, comparison etc. etc.
operator T () const { return reverse(val_); }
};
Целые числа (как математическое понятие) имеют понятие положительных и отрицательных чисел. Эта абстрактная концепция знака имеет ряд различных реализаций в аппаратных средствах.
Endianness не математическое понятие. Little-endian — это аппаратная реализация для улучшения производительности многобайтовой целочисленной арифметики с двумя дополнениями на микропроцессоре с 16 или 32-разрядными регистрами и 8-разрядной шиной памяти. Его создание требовало использования термина big-endian для описания всего остального, имеющего одинаковый порядок байтов в регистрах и в памяти.
Абстрактная машина C включает в себя концепцию целых чисел со знаком и без знака, без подробностей — не требуя арифметики с двойным дополнением, 8-битных байтов или способа хранения двоичного числа в памяти.
PS: Я согласен, что совместимость двоичных данных в сети или в памяти / хранилище — это PIA.
Это хороший вопрос, и я часто думал, что что-то подобное будет полезным. Однако вы должны помнить, что C стремится к независимости от платформы, и порядковый номер важен только тогда, когда подобная структура преобразуется в некий базовый макет памяти. Это преобразование может произойти, когда вы, например, преобразуете буфер uint8_t в int. Хотя модификатор порядка байтов выглядит аккуратно, программист все же должен учитывать другие различия платформы, такие как размеры int, выравнивание структуры и упаковки.
Для защитного программирования, когда вы хотите найти зернистый контроль над тем, как некоторые переменные или структуры представлены в буфере памяти, лучше всего кодировать явные функции преобразования и затем позволить оптимизатору компилятора генерировать наиболее эффективный код для каждой поддерживаемой платформы.
Порядковый номер по сути не является частью типа данных, а скорее его структуры хранения.
Таким образом, это не будет похоже на знаковый / неподписанный, а скорее на ширину битового поля в структурах. Подобно им, они могут быть использованы для определения двоичных API.
Так что у вас было бы что-то вроде
int ip : big 32;
который определит как макет хранилища, так и целочисленный размер, предоставляя компилятору наилучшую работу по согласованию использования поля с его доступом. Для меня не очевидно, какими должны быть разрешенные декларации.
Краткий ответ: если не должно быть возможности использовать объекты в арифметических выражениях (без перегруженных операторов) с участием целых чисел, то эти объекты не должны быть целочисленными типами. И нет смысла допускать сложение и умножение целых и младших порядковых чисел в одном выражении.
Более длинный ответ:
Как кто-то упомянул, порядок байтов зависит от процессора. Что на самом деле означает, что именно так представляются числа, когда они используются как числа на машинном языке (как адреса и как операнды / результаты арифметических операций).
То же самое относится и к обозначениям. Но не до такой же степени. Преобразование из языково-семантического обозначения в одобренное процессором обозначение — это то, что необходимо сделать, чтобы использовать числа в качестве чисел. Преобразование из формата с прямым порядком байтов в порядок с обратным порядком байтов и обратно — это то, что необходимо сделать, чтобы использовать числа в качестве данных (отправлять их по сети или представлять метаданные о данных, передаваемых по сети, например длины полезной нагрузки).
Сказав это, это решение, как представляется, в основном определяется вариантами использования. Обратной стороной является то, что есть хорошая прагматическая причина игнорировать определенные варианты использования. Прагматизм проистекает из того факта, что преобразование порядка байтов обходится дороже, чем большинство арифметических операций.
Если бы язык имел семантику для сохранения чисел в порядке байтов, он позволял бы разработчикам стрелять себе в ногу, заставляя числа с прямым порядком байтов в программе, которая выполняет большую арифметику. Если бы он был разработан на машине с прямым порядком байтов, такое принудительное использование порядка байтов было бы бесполезным. Но при портировании на машину с прямым порядком байтов будет много неожиданных замедлений. И если бы указанные переменные использовались как для арифметики, так и для сетевых данных, это сделало бы код полностью непереносимым.
Отсутствие этой порядковой семантики или форсирование их явным образом зависящим от компилятора вынуждает разработчиков пройти мысленный шаг, думая, что числа «читаются» или «записываются» в / из сетевого формата. Это сделало бы код, который конвертирует туда и обратно между порядком байтов сети и хоста, в середине арифметических операций, громоздким и менее вероятным, чтобы быть предпочтительным способом написания ленивым разработчиком.
А поскольку развитие — это человеческое начинание, делать плохой выбор неудобно — это хорошо.
редактироватьВот пример того, как это может пойти плохо:
Предполагать тот little_endian_int32
а также big_endian_int32
типы вводятся. затем little_endian_int32(7) % big_endian_int32(5)
это постоянное выражение. Каков его результат? Числа неявно преобразованы в собственный формат? Если нет, какой тип результата? Что еще хуже, какова ценность результата (который в этом случае, вероятно, должен быть одинаковым на каждой машине)?
Опять же, если многобайтовые числа используются в качестве простых данных, то массивы символов также хороши. Даже если они являются «портами» (которые на самом деле являются значениями поиска в таблицах или их хэшах), они представляют собой просто последовательности байтов, а не целочисленные типы (по которым можно выполнять арифметику).
Теперь, если вы ограничите разрешенные арифметические операции над числами с прямым порядком байтов только теми операциями, которые разрешены для типов указателей, тогда у вас может быть лучший случай для предсказуемости. затем myPort + 5
на самом деле имеет смысл, даже если myPort
объявлен как что-то вроде little_endian_int16
на большой байтовой машине. То же самое для lastPortInRange - firstPortInRange + 1
, Если арифметика работает так же, как и для типов указателей, то это будет делать то, что вы ожидаете, но firstPort * 10000
было бы незаконно.
Затем, конечно, вы попадаете в аргумент о том, оправдано ли расширение возможностей какой-либо возможной выгодой.
С прагматичной точки зрения программиста, ищущего переполнение стека, стоит отметить, что на этот вопрос можно ответить с помощью служебной библиотеки. Boost имеет такую библиотеку:
http://www.boost.org/doc/libs/1_65_1/libs/endian/doc/index.html
Особенностью библиотеки, наиболее похожей на обсуждаемую функцию языка, является набор арифметических типов, таких как big_int16_t
,
Потому что никто не предлагал добавить его в стандарт и / или потому, что разработчик компилятора никогда не чувствовал в этом необходимости.
Может быть, вы могли бы предложить это комитету. Я не думаю, что это сложно реализовать в компиляторе: компиляторы уже предлагают фундаментальные типы, которые не являются фундаментальными типами для целевой машины.
Разработка C ++ — дело всех кодеров C ++.
@Schimmel. Не слушайте людей, которые оправдывают статус-кво! Все приведенные аргументы, оправдывающие это отсутствие, более чем хрупки. Студент-логик мог найти свое несоответствие, ничего не зная о компьютерных науках. Просто предложите это, и просто не волнуйтесь о патологических консерваторах. (Посоветуйте: предлагайте новые типы, а не классификатор, потому что unsigned
а также signed
ключевые слова считаются ошибками).