Почему в C++ отсутствует модификатор endianness, как для подписи?

(Я думаю, этот вопрос может относиться ко многим типизированным языкам, но я решил использовать 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++ или любого другого языка, который я знаю, и я не нашел никакого обсуждения по этому поводу в Интернете.

Обновить

Я постараюсь обобщить некоторые выводы из множества комментариев, которые я получил в первый час после того, как спросил:

  1. Подписанность является строго двоичной (подписанной или неподписанной) и всегда будет, в отличие от порядка байтов, который также имеет два хорошо известных варианта (большой и маленький), но также и менее известные варианты, такие как смешанный / средний порядок байтов. Новые варианты могут быть изобретены в будущем.
  2. Порядок байтов при обращении к многобайтовым значениям имеет значение. Есть много аспектов, помимо порядка байтов, которые влияют на структуру памяти многобайтовых структур, поэтому такой доступ в основном не рекомендуется.
  3. C++ нацелен на абстрактную машину и минимизирует количество предположений о реализации. Эта абстрактная машина не имеет порядка байтов.

Кроме того, теперь я понимаю, что подписанность и порядковый номер не являются идеальной аналогией, потому что:

  • Endianness определяет только то, как что-то представляется в виде двоичной последовательности, но теперь, что может быть представлено. И то и другое big int а также little int будет иметь точно такой же диапазон значений.
  • подпись определяет, как биты и фактические значения отображаются друг на друга, но также влияет на то, что может быть представлено, например, -3 не может быть представлено unsigned char и (при условии, что char имеет 8 бит) 130 не может быть представлен signed char,

Так что изменение порядкового номера некоторых переменных никогда не изменит поведение программы (за исключением побайтного доступа), тогда как изменение подписи обычно будет.

9 ответов

Что говорит стандарт

[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 Msgstr "Представление значений типов с плавающей точкой определяется реализацией".

В дополнение к ответу 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)) или какое-то другое специфичное для компилятора расширение
  • фактический формат (подпись в 1 или 2 с, компоновка типа с плавающей запятой, представления ловушек)

В качестве альтернативы, вы можете просто захотеть отразить последовательность некоторых указанных аппаратных средств - но 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.

Краткий ответ: если не должно быть возможности использовать объекты в арифметических выражениях (без перегруженных операторов) с участием целых чисел, то эти объекты не должны быть целочисленными типами. И нет смысла допускать сложение и умножение целых и младших порядковых чисел в одном выражении.

Более длинный ответ:

Как кто-то упомянул, порядок байтов зависит от процессора. Что на самом деле означает, что именно так представляются числа, когда они используются как числа на машинном языке (как адреса и как операнды / результаты арифметических операций).

То же самое относится и к обозначениям. Но не до такой же степени. Преобразование из языково-семантического обозначения в одобренное процессором обозначение - это то, что необходимо сделать, чтобы использовать числа в качестве чисел. Преобразование из формата с прямым порядком байтов в порядок с обратным порядком байтов и обратно - это то, что необходимо сделать, чтобы использовать числа в качестве данных (отправлять их по сети или представлять метаданные о данных, передаваемых по сети, например длины полезной нагрузки).

Сказав это, это решение, как представляется, в основном определяется вариантами использования. Обратной стороной является то, что есть хорошая прагматическая причина игнорировать определенные варианты использования. Прагматизм проистекает из того факта, что преобразование порядка байтов обходится дороже, чем большинство арифметических операций.

Если бы язык имел семантику для сохранения чисел в порядке байтов, он позволял бы разработчикам стрелять себе в ногу, заставляя числа с прямым порядком байтов в программе, которая выполняет большую арифметику. Если бы он был разработан на машине с прямым порядком байтов, такое принудительное использование порядка байтов было бы бесполезным. Но при портировании на машину с прямым порядком байтов будет много неожиданных замедлений. И если бы указанные переменные использовались как для арифметики, так и для сетевых данных, это сделало бы код полностью непереносимым.

Отсутствие этой порядковой семантики или форсирование их явным образом зависящим от компилятора вынуждает разработчиков пройти мысленный шаг, думая, что числа "читаются" или "записываются" в / из сетевого формата. Это сделало бы код, который конвертирует туда и обратно между порядком байтов сети и хоста, в середине арифметических операций, громоздким и менее вероятным, чтобы быть предпочтительным способом написания ленивым разработчиком.

А поскольку развитие - это человеческое начинание, делать плохой выбор неудобно - это хорошо.

Изменить: вот пример того, как это может пойти плохо:Предположим, что 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 было бы незаконно.

Затем, конечно, вы попадаете в аргумент о том, оправдано ли расширение возможностей какой-либо возможной выгодой.

Это хороший вопрос, и я часто думал, что что-то подобное будет полезным. Однако вы должны помнить, что C стремится к независимости от платформы, и порядковый номер важен только тогда, когда подобная структура преобразуется в некий базовый формат памяти. Это преобразование может произойти, когда вы, например, преобразуете буфер uint8_t в int. Хотя модификатор порядка байтов выглядит аккуратно, программист все же должен учитывать другие различия платформы, такие как размеры int, выравнивание структуры и упаковки. Для защитного программирования, когда вы хотите найти зернистый контроль над тем, как некоторые переменные или структуры представлены в буфере памяти, лучше всего кодировать явные функции преобразования и затем позволить оптимизатору компилятора генерировать наиболее эффективный код для каждой поддерживаемой платформы.

Порядковый номер по сути не является частью типа данных, а скорее его структуры хранения.

Таким образом, это не будет похоже на знаковый / неподписанный, а скорее на ширину битового поля в структурах. Подобно им, они могут быть использованы для определения двоичных API.

Так что у вас было бы что-то вроде

int ip : big 32;

который определит как макет хранилища, так и целочисленный размер, предоставляя компилятору наилучшую работу по согласованию использования поля с его доступом. Для меня не очевидно, какими должны быть разрешенные декларации.

С прагматичной точки зрения программиста, ищущего переполнение стека, стоит отметить, что на этот вопрос можно ответить с помощью служебной библиотеки. Boost имеет такую ​​библиотеку:

http://www.boost.org/doc/libs/1_65_1/libs/endian/doc/index.html

Особенностью библиотеки, наиболее похожей на обсуждаемую функцию языка, является набор арифметических типов, таких как big_int16_t,

Потому что никто не предлагал добавить его в стандарт и / или потому, что разработчик компилятора никогда не чувствовал в этом необходимости.

Может быть, вы могли бы предложить это комитету. Я не думаю, что это сложно реализовать в компиляторе: компиляторы уже предлагают фундаментальные типы, которые не являются фундаментальными типами для целевой машины.

Разработка C++ - дело всех кодеров C++.

@Schimmel. Не слушайте людей, которые оправдывают статус-кво! Все приведенные аргументы, оправдывающие это отсутствие, более чем хрупки. Студент-логик мог найти свое несоответствие, ничего не зная о компьютерных науках. Просто предложите это, и просто не волнуйтесь о патологических консерваторах. (Посоветуйте: предлагайте новые типы, а не классификатор, потому что unsigned а также signed ключевые слова считаются ошибками).

Endianness специфичен для компилятора в результате его специфичности для машины, а не как механизм поддержки независимости платформы. Стандарт - это абстракция, которая не учитывает навязывание правил, которые делают вещи "простыми" - его задача состоит в том, чтобы создать сходство между компиляторами, которое позволяет программисту создавать "независимость от платформы" для своего кода - если они решат сделать это так.

Первоначально существовала большая конкуренция между платформами за долю рынка, а также - компиляторы чаще всего создавались производителями микропроцессоров в качестве проприетарных инструментов и для поддержки операционных систем на определенных аппаратных платформах. Вероятно, Intel не очень заботилась о написании компиляторов, поддерживающих микропроцессоры Motorola.

В конце концов, Bell Labs изобрел C для переписывания Unix.

Другие вопросы по тегам