Почему C ++ имеет «неопределенное поведение» (UB), а другие языки, такие как C # или Java, не имеют?


50

В этом посте с переполнением стека приведен довольно полный список ситуаций, в которых спецификация языка C / C ++ объявляется как «неопределенное поведение». Однако я хочу понять, почему в других современных языках, таких как C # или Java, нет понятия «неопределенное поведение». Означает ли это, что конструктор компилятора может управлять всеми возможными сценариями (C # и Java) или нет (C и C ++)?




3
и все же этот пост SO ссылается на неопределенное поведение даже в спецификации Java!
gbjbaanb

«Почему в C ++ есть« неопределенное поведение »?» К сожалению, кажется, что это один из тех вопросов, на которые сложно ответить объективно, помимо утверждения «потому что по причинам X, Y и / или Z (все из которых могут быть nullptr) нет кто-то удосужился определить поведение, написав и / или приняв предложенную спецификацию ». : c
code_dredd

Я бы оспорил предпосылку. По крайней мере, C # имеет «небезопасный» код. Microsoft пишет «В некотором смысле написание небезопасного кода очень похоже на написание кода C в программе на C #» и приводит примеры причин, по которым можно это сделать: для доступа к оборудованию или ОС и для скорости. Это то, для чего был изобретен C (черт, они написали ОС на C!), Так что у вас это есть.
Питер - восстановить Монику

Ответы:


72

Неопределенное поведение - одна из тех вещей, которые были признаны очень плохой идеей только в ретроспективе.

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

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

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

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

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


56
Я не думаю, что многие люди из сообщества C согласились бы с этим утверждением. Если вы модернизируете C и определяете неопределенное поведение (например, инициализируйте все по умолчанию, выбираете порядок оценки для параметра функции и т. Д.), Большая база кода с хорошим поведением продолжит работать на отлично. Только код, который не будет четко определен сегодня, будет нарушен. С другой стороны, если вы оставите неопределенным, как сегодня, компиляторы будут по-прежнему свободны в использовании новых достижений в архитектуре ЦП и оптимизации кода.
Кристоф

13
Основная часть ответа не очень убедительна для меня. Я имею в виду, что в принципе невозможно написать функцию, которая безопасно добавляет два числа (как в int32_t add(int32_t x, int32_t y)) в C ++. Обычные аргументы вокруг этого связаны с эффективностью, но часто перемежаются с некоторыми аргументами переносимости (как в «Один раз напиши, запусти ... на платформе, где ты это написал ... и нигде больше ;-)»). Грубо говоря, один из аргументов может быть следующим: некоторые вещи не определены, потому что вы не знаете, используете ли вы 16-битный микроконтроллер или 64-битный сервер (слабый, но все еще аргумент)
Marco13,

12
@ Marco13 Согласен - и избавиться от проблемы «неопределенного поведения», сделав что-то «определенное поведение, но не обязательно то, что хотел пользователь, и без предупреждения, когда это происходит» вместо «неопределенного поведения» - просто играть в игры адвокат кода IMO ,
алефзеро

9
«Даже сегодня большинство утечек в системе безопасности от крошечных до ужасных являются результатом неопределенного поведения в той или иной форме». Нужна цитата. Я думал, что большинство из них были инъекции XYZ сейчас.
Джошуа

34
«Неопределенное поведение - одна из тех вещей, которые были признаны очень плохой идеей только в ретроспективе». Это твое мнение. Многие (включая меня) не разделяют это.
Легкость гонок с Моникой

103

В основном потому, что разработчики Java и подобных языков не хотели неопределенного поведения в своем языке. Это был компромисс - разрешение неопределенного поведения может повысить производительность, но разработчики языка отдали приоритет безопасности и предсказуемости выше.

Например, если вы выделите массив в C, данные не определены. В Java все байты должны быть инициализированы в 0 (или другое указанное значение). Это означает, что среда выполнения должна проходить через массив (операция O (n)), в то время как C может выполнить выделение в одно мгновение. Так что C всегда будет быстрее для таких операций.

Если код, использующий массив, в любом случае заполнит его перед чтением, то для Java это в основном бесполезное усилие. Но в случае, когда код читается первым, вы получаете предсказуемые результаты в Java, но непредсказуемые результаты в C.


19
Отличная презентация дилеммы HLL: безопасность и простота использования и производительность. Там нет серебряной пули: есть варианты использования для каждой стороны.
Кристоф

5
@ Christophe Чтобы быть честным, есть гораздо лучшие подходы к проблеме, чем позволить UB остаться полностью неоспоримым, как C и C ++. Вы можете иметь безопасный, управляемый язык с аварийными люками на небезопасной территории, чтобы вы могли подать заявку там, где это выгодно. TBH, было бы очень приятно просто скомпилировать мою программу на C / C ++ с флагом, который говорит: «вставьте любое дорогостоящее оборудование, которое вам нужно, мне все равно, но просто расскажите мне обо ВСЕХ UB, которые происходят» «.
Александр

4
Хорошим примером структуры данных, которая намеренно считывает неинициализированные местоположения, является представление разреженного набора Бриггса и Торсона (например, см. Codingplayground.blogspot.com/2009/03/… ). Инициализация такого набора - это O (1) в C, но O ( n) с принудительной инициализацией Java.
Арка Д. Робисона

9
Хотя верно то, что принудительная инициализация данных делает сломанные программы намного более предсказуемой, это не гарантирует предполагаемого поведения: если алгоритм ожидает чтения значимых данных при ошибочном чтении неявно инициализированного нуля, это такая же ошибка, как если бы он имел читай мусор. В программе на C / C ++ такая ошибка будет видна при запуске процесса valgrind, который покажет, где именно было использовано неинициализированное значение. Вы не можете использовать valgrindкод Java, потому что среда выполнения выполняет инициализацию, делая valgrindпроверки s бесполезными.
Cmaster

5
@cmaster Вот почему компилятор C # не позволяет вам читать с неинициализированных локальных пользователей. Не нужно проверок во время выполнения, не нужно инициализировать, просто анализ во время компиляции. Однако это все еще компромисс - в некоторых случаях у вас нет хорошего способа справиться с ветвлением вокруг потенциально неназначенных местных жителей. На практике я не обнаружил ни одного случая, когда бы этот проект не был плохим в первую очередь и лучше решался путем переосмысления кода, чтобы избежать сложного ветвления (которое трудно анализировать людям), но это по крайней мере возможно.
Луан

42

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

Смотрите http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

Использование неинициализированной переменной: это обычно известно как источник проблем в программах на Си, и существует множество инструментов для их обнаружения: от предупреждений компилятора до статических и динамических анализаторов. Это повышает производительность, так как не требует, чтобы все переменные были инициализированы нулем, когда они входят в область действия (как это делает Java). Для большинства скалярных переменных это может привести к небольшим накладным расходам, но массивы стека и память malloc'd повлекут за собой набор памяти хранилища, что может быть довольно дорогостоящим, особенно потому, что хранилище обычно полностью перезаписывается.


Целочисленное переполнение со знаком: если арифметика с типом 'int' (например) переполняется, результат не определен. Одним из примеров является то, что "INT_MAX + 1" не обязательно будет INT_MIN. Такое поведение включает определенные классы оптимизации, которые важны для некоторого кода. Например, знание того, что INT_MAX + 1 не определено, позволяет оптимизировать «X + 1> X» до «true». Знание, что умножение «не может» переполниться (потому что это было бы неопределенным), позволяет оптимизировать «X * 2/2» до «X». Хотя это может показаться тривиальным, такого рода вещи обычно раскрываются при помощи встраивания и расширения макросов. Более важная оптимизация, которую это позволяет, заключается в циклах "<=":

for (i = 0; i <= N; ++i) { ... }

В этом цикле компилятор может предположить, что цикл будет повторяться ровно N + 1 раз, если «i» не определено при переполнении, что позволяет задействовать широкий диапазон оптимизаций цикла. С другой стороны, если переменная определена как Обернувшись при переполнении, компилятор должен предположить, что цикл, возможно, бесконечен (что происходит, если N равно INT_MAX), что затем отключает эти важные оптимизации цикла. Это особенно касается 64-битных платформ, так как очень много кода использует «int» в качестве индукционных переменных.


27
Конечно, настоящая причина, по которой переполнение со знаком со знаком не определено, заключается в том, что при разработке C использовалось как минимум три различных представления целых чисел со знаком (одно-дополнение, два-дополнение, знаковая величина и, возможно, двоичное смещение) и каждый из них дает свой результат для INT_MAX + 1. Создание переполнения неопределенным позволяет a + bкомпилировать в нативную add b aинструкцию в любой ситуации, вместо того, чтобы потенциально требовать от компилятора имитировать какую-то другую форму целочисленной арифметики со знаком.
Марк

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

5
@supercat. Это еще одна причина, по которой избегание неопределенного поведения более распространено в более поздних языках - время программиста оценивается намного больше, чем время процессора. Оптимизация, которую C позволяет выполнять благодаря UB, по сути бессмысленна на современных настольных компьютерах и значительно усложняет рассуждения о коде (не говоря уже о последствиях для безопасности). Даже в коде, критически важном для производительности, вы можете получить выгоду от высокоуровневых оптимизаций, которые было бы несколько сложнее (или даже намного сложнее) сделать в C. У меня есть собственный программный 3D-рендерер в C #, и возможность использовать, например, a HashSetзамечательна.
Луан

2
@supercat: Wrt_looselyопределен_, логическим выбором для целочисленного переполнения будет требование поведения, определяемого реализацией . Это существующая концепция, и она не является чрезмерной нагрузкой на реализацию. Я подозреваю, что большинству сойдет с рук «это 2-е дополнение с обертыванием». <<может быть трудный случай.
MSalters

@MSalters Существует простое и хорошо изученное решение, которое не является ни неопределенным поведением, ни поведением, определяемым реализацией: недетерминированное поведение. То есть, вы можете сказать « x << yоценивает какое-то допустимое значение типа, int32_tно мы не будем говорить, какой». Это позволяет разработчикам использовать быстрое решение, но не действует как ложное предварительное условие, позволяющее оптимизировать стиль путешествий во времени, поскольку недетерминизм ограничен выводом этой одной операции - спецификация гарантирует, что память, переменные и т. Д. Не будут видны незаметно по оценке выражения. ...
Марио Карнейру

20

В первые дни C было много хаоса. Различные компиляторы относились к языку по-разному. Когда было интересно написать спецификацию для языка, эта спецификация должна была быть достаточно обратно совместимой с C, на который программисты полагались со своими компиляторами. Но некоторые из этих деталей непереносимы и не имеют общего смысла, например, предполагают конкретную последовательность или порядок данных. Таким образом, стандарт C сохраняет много деталей в виде неопределенного или заданного реализацией поведения, что оставляет большую гибкость разработчикам компиляторов. C ++ основан на C, а также имеет неопределенное поведение.

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


Итак, вы говорите, что обратная совместимость - единственная причина, почему C и C ++ не выходят из неопределенного поведения?
Сисир

3
Это определенно один из самых больших, @Sisir. Даже среди опытных программистов вы будете удивлены, как много вещей, которые не должны ломаться , ломаются, когда компилятор меняет способ обработки неопределенного поведения. (В данном случае, был некоторый хаос, когда GCC начал оптимизировать «is thisnull?», Проверяет некоторое время назад, на том основании, что thisбытие было nullptrUB, и, следовательно, никогда не может произойти.)
Джастин Тайм 2 Восстановите Монику

9
@Sisir, еще один важный момент - скорость. В первые дни C оборудование было гораздо более разнородным, чем сегодня. Просто не указав, что происходит, когда вы добавляете 1 к INT_MAX, вы можете позволить компилятору делать все, что быстрее для архитектуры (например, система с одним дополнением будет производить -INT_MAX, а система с двумя дополнениями будет производить INT_MIN). Точно так же, не указывая, что происходит, когда вы читаете за концом массива, вы можете заставить систему с защитой памяти завершать программу, в то время как без нее не нужно будет выполнять дорогостоящую проверку границ во время выполнения.
Марк

14

В языках JVM и .NET это легко:

  1. Они не должны иметь возможность работать напрямую с оборудованием.
  2. Они должны работать только с современными настольными и серверными системами или достаточно похожими устройствами, или, по крайней мере, с устройствами, разработанными для них.
  3. Они могут навязать сборщик мусора для всей памяти и принудительную инициализацию, таким образом получая безопасность указателя.
  4. Они были определены одним актером, который также обеспечил единственную окончательную реализацию.
  5. Они выбирают безопасность, а не производительность.

Есть хорошие моменты для выбора, хотя:

  1. Системное программирование - это совершенно другая игра, и вместо этого целесообразно бескомпромиссная оптимизация для прикладного программирования.
  2. По общему признанию, все время есть менее экзотическое оборудование, но небольшие встроенные системы здесь, чтобы остаться.
  3. GC плохо подходит для не заменимых ресурсов и торгует гораздо большим пространством для хорошей производительности. И большинство (но не все) принудительные инициализации могут быть оптимизированы.
  4. Есть преимущества для большей конкуренции, но комитеты означают компромисс.
  5. Все эти границы провер действительно складывается, хотя большинство из них может быть оптимизировано прочь. Проверка нулевого указателя в основном может быть выполнена путем захвата доступа для нулевых издержек благодаря виртуальному адресному пространству, хотя оптимизация все еще запрещена.

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


3
На самом деле. Я программирую на C # для моей работы. Время от времени я берусь за один из небезопасных молотков ( unsafeключевое слово или атрибуты в System.Runtime.InteropServices). Предоставляя этот материал нескольким программистам, которые знают, как отлаживать неуправляемые вещи, и, опять же, настолько мало, насколько это практически возможно, мы устраняем проблемы. Прошло более 10 лет с момента последнего небезопасного молотка, связанного с производительностью, но иногда это нужно делать, потому что другого решения буквально нет.
Джошуа

19
Я часто работаю на платформе с аналоговыми устройствами, где sizeof (char) == sizeof (short) == sizeof (int) == sizeof (float) == 1. Это также делает насыщающее сложение (поэтому INT_MAX + 1 == INT_MAX) и что хорошо в C, так это то, что у меня может быть соответствующий компилятор, который генерирует разумный код. Если в обязательном порядке говорится, что двойки дополняются переносом, то каждое добавление будет заканчиваться тестом и ветвью, что является чем-то вроде незапуска в сфокусированной на DSP части. Это текущая часть производства.
Дэн Миллс

5
@BenVoigt Некоторые из нас живут в мире, где небольшой компьютер занимает, возможно, 4 КБ пространства кода, фиксированный стек вызова / возврата 8 уровней, 64 байта ОЗУ, тактовую частоту 1 МГц и стоит менее $ 0,20 в количестве 1000. Современный мобильный телефон - это небольшой ПК с практически неограниченным хранилищем для любых целей и задач, и его можно рассматривать как ПК. Не весь мир многоядерный и не имеет жестких ограничений в реальном времени.
Дэн Миллс

2
@DanMills: Не говоря уже о современных мобильных телефонах с процессорами Arm Cortex A, мы говорим о «функциональных телефонах» около 2002 года. Да, 192 КБ SRAM - это намного больше, чем 64 байта (что не «маленький», а «маленький»), но 192kB также точно не называют «современным» настольным компьютером или сервером в течение 30 лет. Также в эти дни 20 центов принесут вам MSP430 с более чем 64 байтами SRAM.
Бен Фойгт

2
@BenVoigt 192kB, возможно, не был настольным компьютером за последние 30 лет, но я могу заверить вас, что вполне достаточно для обслуживания веб-страниц, что, я бы сказал, делает такую ​​вещь сервером по самому определению этого слова. Фактом является то, что это вполне разумное (щедрое, даже) количество оперативной памяти для множества встроенных приложений, которые часто включают веб-серверы конфигурации. Конечно, я, вероятно, не запускаю amazon на нем, но я мог бы просто запустить холодильник с IOT-программным обеспечением на таком ядре (со временем и свободным пространством). Никому не нужны для этого интерпретируемые или JIT языки!
Дэн Миллс

8

Java и C # характеризуются доминирующим вендором, по крайней мере, на раннем этапе их разработки. (Sun и Microsoft соответственно). C и C ++ разные; у них было несколько конкурирующих реализаций с самого начала. Особенно C работал на экзотических аппаратных платформах. В результате произошли различия между реализациями. Комитеты ISO, которые стандартизировали C и C ++, могли бы согласовать большой общий знаменатель, но на краях, где реализации отличаются, стандарты оставляли место для реализации.

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


Что буквально означает «большой общий знаменатель» ? Вы говорите о подмножествах или суперсетах? Вы действительно имеете в виду достаточно общих факторов? Это как наименьшее общее множитель или самый большой общий фактор? Это очень сбивает с толку нас, роботов, которые не говорят на уличном языке, только по математике. :)
4

@tchrist: обычное поведение - это подмножество, но это подмножество довольно абстрактно. Во многих областях, которые не определены общим стандартом, реальные реализации должны сделать выбор. Теперь некоторые из этих вариантов довольно ясны и, следовательно, определяются реализацией, но другие более расплывчаты. Разметка памяти во время выполнения пример: там должен быть выбор, но это не ясно , как вы бы документировать.
MSalters

2
Оригинальный C был сделан одним парнем. У него уже было много UB, по замыслу. Ситуация, конечно, ухудшилась, когда C стал популярным, но UB был там с самого начала. Паскаль и Smalltalk имели гораздо меньше UB и были разработаны почти в одно и то же время. Основным преимуществом C было то, что его было очень легко портировать - все проблемы переносимости были делегированы программисту приложения: P Я даже перенес простой компилятор C на мой (виртуальный) процессор; сделать что-то вроде LISP или Smalltalk было бы гораздо труднее (хотя у меня был ограниченный прототип для среды выполнения .NET :).
Луан

@Luaan: это будет Керниган или Ричи? И нет, у него не было неопределенного поведения. Я знаю, у меня на столе лежала оригинальная документация компилятора AT & T. Реализация сделала то, что сделала. Не было никакого различия между неопределенным и неопределенным поведением.
MSalters

4
@MSalters Ритчи был первым парнем. Керниган присоединился (не намного) позже. Ну, у него не было «неопределенного поведения», потому что этот термин еще не существовал. Но у него было то же поведение, которое сегодня будет называться неопределенным. Поскольку у C не было спецификации, даже «неопределенное» - это очень тяжело :) Это было просто то, о чем не заботился компилятор, а детали были за программистами приложений. Он не был предназначен для создания переносимых приложений , только компилятор должен был легко переноситься .
Луаан

6

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

Хотя они в значительной степени забыты (а их существование иногда даже отрицается), самые первые версии UNIX были написаны на ассемблере. Большая часть (если не только) первоначальной цели C была переносом UNIX с языка ассемблера на язык более высокого уровня. Часть намерения состояла в том, чтобы написать как можно больше операционной системы на языке более высокого уровня - или смотреть на это с другой стороны, чтобы минимизировать количество, которое должно было быть написано на языке ассемблера.

Для этого C необходимо было обеспечить почти такой же уровень доступа к оборудованию, как и на языке ассемблера. PDP-11 (для одного примера) сопоставляет регистры ввода / вывода с конкретными адресами. Например, вы прочитали одну ячейку памяти, чтобы проверить, была ли нажата клавиша на системной консоли. В этом месте был установлен один бит, когда были данные, ожидающие чтения. Затем вы читаете байт из другого указанного местоположения, чтобы получить код ASCII нажатой клавиши.

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

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

Конечно, это имеет довольно серьезную проблему: не у каждой машины на Земле есть память, идентичная PDP-11 начала 1970-х годов. Таким образом, когда вы берете это целое число, конвертируете его в указатель, а затем читаете или пишете через этот указатель, никто не может дать разумную гарантию того, что вы собираетесь получить. Просто для наглядного примера, чтение и запись могут отображаться в отдельные регистры аппаратного обеспечения, поэтому вы (в отличие от обычной памяти), если вы что-то пишете, затем пытаетесь прочитать это обратно, то, что вы читаете, может не соответствовать тому, что вы написали.

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

  1. Определите интерфейс для всех возможных аппаратных средств - укажите абсолютные адреса всех местоположений, которые вы, возможно, захотите прочитать или записать, чтобы каким-либо образом взаимодействовать с оборудованием.
  2. Запретите этот уровень доступа и постановьте, что любой, кто хочет делать такие вещи, должен использовать язык ассемблера.
  3. Позвольте людям делать это, но предоставьте им право читать (например) руководства для оборудования, на которое они нацелены, и писать код, соответствующий используемому оборудованию.

Из них 1 кажется достаточно нелепым, что вряд ли стоит его обсуждать. 2 в основном отбрасывает основные намерения языка. Это оставляет третий вариант, по сути, единственным, который они могут разумно рассмотреть вообще.

Другой вопрос, который встречается довольно часто, это размеры целочисленных типов. C занимает «позицию», которая intдолжна быть естественного размера, предложенного архитектурой. Таким образом, если я программирую 32-битный VAX, он, intвероятно , должен быть 32-битным , но если я программирую 36-битный Univac, intвероятно , должен быть 36 бит (и так далее). Вероятно, нецелесообразно (и может даже не быть возможным) писать операционную систему для 36-битного компьютера, используя только типы, которые гарантированно будут кратны размеру 8 бит. Может быть, я просто поверхностен, но мне кажется, что если бы я писал ОС для 36-битной машины, я бы, вероятно, хотел бы использовать язык, который поддерживает 36-битный тип.

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

Java и C # могут игнорировать все это. Они не предназначены для поддержки написания операционных систем. С ними у вас есть пара вариантов. Один из них заключается в том, чтобы обеспечить аппаратную поддержку тем, что им требуется - поскольку они требуют типов 8, 16, 32 и 64 бита, просто создайте оборудование, которое поддерживает эти размеры. Другая очевидная возможность заключается в том, что язык может работать только поверх другого программного обеспечения, которое обеспечивает необходимую среду, независимо от того, что может потребоваться базовое оборудование.

В большинстве случаев это на самом деле не выбор. Скорее, многие реализации делают мало того и другого. Обычно вы запускаете Java на JVM, работающей в операционной системе. Чаще всего ОС написана на C, а JVM на C ++. Если JVM работает на процессоре ARM, вполне вероятно, что процессор включает в себя расширения Jazelle ARM, чтобы адаптировать аппаратное обеспечение более близко к потребностям Java, поэтому в программном обеспечении требуется меньше, а код Java работает быстрее (или меньше). все равно медленно)

Резюме

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


4

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

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


Я чувствую, что вы описали здесь что-то важное, но это ускользает от меня. Не могли бы вы уточнить свой ответ? Особенно второй абзац: в нем говорится, что условия сейчас и условия ранее разные, но я не понимаю; что именно изменилось? Кроме того, «путь» теперь отличается от ранее; может это тоже объяснить?
Анатолий

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

1
@anatolyg: Если вы этого еще не сделали, прочитайте опубликованный документ C Rationale (введите C99 Rationale в Google). Строки 11-29 в стр. 11-29 говорят о «рынке», а строки 5-8 в стр. 13 говорят о том, что предназначено для переносимости. Как вы думаете, как начальник коммерческой компании по компиляции отреагирует, если автор компилятора скажет программистам, которые жаловались, что оптимизатор нарушает код, который любой другой компилятор обрабатывает с пользой, что их код «сломан», потому что он выполняет действия, не определенные Стандартом, и отказался поддержать его, потому что это способствовало бы продолжению ...
Суперкат

1
... использование таких конструкций? Такая точка зрения очевидна на досках поддержки clang и gcc, и она помешала разработке встроенных функций, которые могли бы облегчить оптимизацию намного проще и безопаснее, чем хотят поддерживать сломанный язык gcc и clang.
Суперкат

1
@supercat: Вы зря тратите время, жалуясь поставщикам компиляторов. Почему бы не направить ваши проблемы в языковые комитеты? Если они согласятся с вами, будут опубликованы опечатки, которые вы можете использовать для победы над командами компиляторов. И этот процесс намного быстрее, чем разработка новой версии языка. Но если они не согласны, вы, по крайней мере, получите реальные причины, тогда как авторы компилятора просто повторят (снова и снова) «Мы не определяли этот код как нарушенный, это решение было принято языковым комитетом, и мы следовать их решению. "
Бен Фойгт

3

C ++ и c оба имеют описательные стандарты (в любом случае, версии ISO).

Они существуют только для объяснения того, как работают языки, и для предоставления единой ссылки о том, что это за язык. Как правило, производители компиляторов и разработчики библиотек руководят, и некоторые предложения включаются в основной стандарт ISO.

Java и C # (или Visual C #, что, я полагаю, вы имеете в виду) имеют предписывающие стандарты. Они сообщают вам, что на языке определенно опережает время, как он работает и что считается допустимым поведением.

Более того, Java на самом деле имеет «эталонную реализацию» в Open-JDK. (Я думаю, что Roslyn считается эталонной реализацией Visual C #, но не смог найти источник для этого.)

В случае Java, если в стандарте есть неопределенность, и Open-JDK делает это определенным образом. То, как это делает Open-JDK, является стандартом.


Ситуация еще хуже: я не думаю, что Комитет когда-либо достиг консенсуса относительно того, должен ли он быть описательным или предписывающим.
суперкат

1

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

Например, переполнения со знаком являются неопределенным поведением в Си. На практике ожидалось, что компилятор сгенерирует простой код операции добавления со знаком для выполнения ЦП, и поведение будет таким, как этот конкретный ЦП.

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

Это является причиной большей части неопределенного поведения в C, и поэтому такие вещи, как размер, intразличаются в разных системах. Intзависит от архитектуры и обычно выбирается как самый быстрый и самый эффективный тип данных, который больше, чем a char.

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

Более поздние языки, такие как Java и C #, предпочитали устранять неопределенное поведение по сравнению с необработанной производительностью.


-5

В некотором смысле, у Java также есть это. Предположим, вы дали неверный компаратор для Arrays.sort. Он может бросить исключение, если его обнаружит. В противном случае он будет сортировать массив каким-либо образом, который не гарантированно будет конкретным.

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

C ++ просто пошел дальше, чтобы создавать неопределенные дополнительные ситуации (или, скорее, Java решил определить больше операций) и иметь имя для него.


4
Это не неопределенное поведение, о котором мы говорим здесь. «Неправильные компараторы» бывают двух типов: те, которые определяют общий порядок, и те, которые не определяют. Если вы предоставляете компаратор, который последовательно определяет относительный порядок элементов, поведение четко определено, это просто не то поведение, которое хотел программист. Если вы предоставите компаратор, который не соответствует относительному упорядочению, поведение все равно будет четко определено: функция сортировки вызовет исключение (что, вероятно, также не соответствует поведению, которое хотел программист).
Марк

2
Что касается изменения переменных, условия гонки обычно не считаются неопределенным поведением. Я не знаю деталей того, как Java обрабатывает назначения для общих данных, но зная общую философию языка, я почти уверен, что он должен быть атомарным. Одновременное присвоение 53 и 71 aбыло бы неопределенным поведением, если бы вы могли извлечь из него 51 или 73, но если вы можете получить только 53 или 71, оно четко определено.
Марк

@Mark С кусками данных, которые больше, чем собственный размер слова системы (например, 32-битная переменная в 16-битной системе размера слова), возможно иметь архитектуру, которая требует хранения каждой 16-битной части отдельно. (SIMD является еще одной потенциальной такой ситуацией.) В этом случае даже простое присвоение уровня исходного кода не обязательно является атомарным, если компилятор не позаботится о том, чтобы он выполнялся атомарно.
CVn
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.