Подтвердить что ты не робот

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

(Я предполагаю, что этот вопрос может применяться ко многим типизированным языкам, но я решил использовать С++ в качестве примера.)

Почему нет способа просто написать:

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

Update

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

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

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

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

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

4b9b3361

Ответ 1

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

[intro.abstract]/1:

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

С++ не мог определить классификатор соответствия, поскольку он не имеет понятия о значении endianness.

Обсуждение

О различии между знаками и суждениями, OP писал

В моем понимании, сущность - это просто вариация того же принципа [(signness)]: другая интерпретация двоичного паттерна, основанного на информации времени компиляции о памяти, в которой она будет сохранена.

Я бы утверждал, что у знака есть семантический и репрезентативный аспект 1. Что подразумевает [intro.abstract]/1, так это то, что С++ заботится только о семантике и никогда не обращается к тому, как должно быть представлено число со знаком в памяти 2. Фактически, "sign bit" появляется только один раз в спецификациях С++ и ссылается на значение, определенное для реализации.
С другой стороны, утверждение имеет только репрезентативный аспект: endianness не имеет никакого значения.


1) Семантический аспект: целое число со знаком может представлять значения ниже нуля; репрезентативный аспект: нужно, например, зарезервировать немного, чтобы передать положительный/отрицательный знак.
2) В том же ключе С++ никогда не описывает, как должен быть представлен номер с плавающей запятой, часто используется IEEE-754, но это выбор, сделанный реализацией, в любом случае в соответствии со стандартом: [basic.fundamental]/8 "Представление значений типов с плавающей запятой определено по реализации".

Ответ 2

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

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

Ответ 3

Целые числа (как математическое понятие) имеют понятие положительных и отрицательных чисел. Эта абстрактная концепция знака имеет ряд различных реализаций в оборудовании.

Эндианность - не математическое понятие. Little-endian - это трюк с аппаратной реализацией для повышения производительности многобайтовой двунаправленной целочисленной арифметики на микропроцессоре с 16 или 32-битными регистрами и 8-битной шиной памяти. Его создание потребовало использования термина big-endian для описания всего остального, имеющего тот же порядок байтов в регистрах и в памяти.

Абстрактная машина C включает концепцию целых чисел без знака без подробностей - без необходимости арифметики с двумя дополнениями, 8-битных байтов или как сохранить двоичный номер в памяти.

PS: Я согласен, что совместимость двоичных данных в сети или в памяти/хранилище является PIA.

Ответ 4

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

Ответ 5

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

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

Итак, у вас есть что-то вроде

int ip : big 32;

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

Ответ 6

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

Дольше ответ:

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

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

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

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

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

И так как развитие - это человеческое стремление, плохое решение неудобно - это хорошая вещь (TM).

Изменить: вот пример того, как это может пойти плохо: Предположим, что введены типы little_endian_int32 и big_endian_int32. Тогда little_endian_int32(7) % big_endian_int32(5) является постоянным выражением. Каков его результат? Получают ли числа неявно преобразованные в собственный формат? Если нет, каков тип результата? Хуже того, какова ценность результата (который в этом случае, вероятно, должен быть одинаковым на каждой машине)?

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

Теперь, если вы ограничиваете допустимые арифметические операции с явно-конечными числами только теми действиями, которые разрешены для типов указателей, тогда у вас может быть лучший пример для предсказуемости. Тогда myPort + 5 действительно имеет смысл, даже если myPort объявлен как нечто вроде little_endian_int16 на большой конечной машине. То же самое для lastPortInRange - firstPortInRange + 1. Если арифметика работает так же, как и для типов указателей, то это будет делать то, что вы ожидаете, но firstPort * 10000 будет незаконным.

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

Ответ 7

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

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

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

Ответ 8

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

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

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

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

Ответ 9

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

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

C был - в конце концов - изобрел Bell Labs, чтобы переписать Unix.