В этом посте с переполнением стека приведен довольно полный список ситуаций, в которых спецификация языка C / C ++ объявляется как «неопределенное поведение». Однако я хочу понять, почему в других современных языках, таких как C # или Java, нет понятия «неопределенное поведение». Означает ли это, что конструктор компилятора может управлять всеми возможными сценариями (C # и Java) или нет (C и C ++)?
50
nullptr
) нет кто-то удосужился определить поведение, написав и / или приняв предложенную спецификацию ». : cОтветы:
Неопределенное поведение - одна из тех вещей, которые были признаны очень плохой идеей только в ретроспективе.
Первые компиляторы были большими достижениями и радостно приветствовали улучшения по сравнению с альтернативой - машинным языком или программированием на ассемблере. Проблемы с этим были хорошо известны, и языки высокого уровня были изобретены специально для решения этих известных проблем. (В то время энтузиазм был настолько велик, что HLL иногда называли «концом программирования» - как будто теперь нам нужно было бы просто записать то, что мы хотели, и компилятор выполнил бы всю настоящую работу.)
Только позже мы поняли новые проблемы, которые пришли с новым подходом. Удаленность от реальной машины, на которой выполняется код, означает, что есть большая вероятность того, что все будет молчаливо не делать то, что мы ожидали от них. Например, выделение переменной обычно оставляет начальное значение неопределенным; это не считалось проблемой, потому что вы не выделите переменную, если не хотите хранить в ней значение, верно? Конечно, было не слишком много, чтобы ожидать, что профессиональные программисты не забудут присвоить начальное значение, не так ли?
Оказалось, что с более крупными базами кода и более сложными структурами, которые стали возможными с более мощными системами программирования, да, многие программисты действительно время от времени совершали бы такие упущения, и возникающее в результате неопределенное поведение становилось серьезной проблемой. Даже сегодня большинство утечек в системе безопасности от крошечных до ужасных являются результатом неопределенного поведения в той или иной форме. (Причина в том, что обычно неопределенное поведение на самом деле очень сильно определяется вещами на следующем более низком уровне вычислений, и злоумышленники, которые понимают этот уровень, могут использовать это пространство для маневра, чтобы заставить программу выполнять не только непреднамеренные вещи, но и именно они намерены.)
С тех пор как мы это осознали, было общее стремление исключить неопределенное поведение из языков высокого уровня, и Java была особенно внимательна в этом (что было сравнительно легко, так как в любом случае было разработано для работы на собственной специально разработанной виртуальной машине). Старые языки, такие как C, не могут быть легко модифицированы без потери совместимости с огромным количеством существующего кода.
Изменить: Как указано, эффективность является еще одной причиной. Неопределенное поведение означает, что разработчики компиляторов имеют много возможностей для использования целевой архитектуры, так что каждая реализация уходит с максимально быстрой реализацией каждой функции. Это было более важно на вчерашних слабых машинах, чем сегодня, когда зарплата программиста часто является узким местом для разработки программного обеспечения.
источник
int32_t add(int32_t x, int32_t y)
) в C ++. Обычные аргументы вокруг этого связаны с эффективностью, но часто перемежаются с некоторыми аргументами переносимости (как в «Один раз напиши, запусти ... на платформе, где ты это написал ... и нигде больше ;-)»). Грубо говоря, один из аргументов может быть следующим: некоторые вещи не определены, потому что вы не знаете, используете ли вы 16-битный микроконтроллер или 64-битный сервер (слабый, но все еще аргумент)В основном потому, что разработчики Java и подобных языков не хотели неопределенного поведения в своем языке. Это был компромисс - разрешение неопределенного поведения может повысить производительность, но разработчики языка отдали приоритет безопасности и предсказуемости выше.
Например, если вы выделите массив в C, данные не определены. В Java все байты должны быть инициализированы в 0 (или другое указанное значение). Это означает, что среда выполнения должна проходить через массив (операция O (n)), в то время как C может выполнить выделение в одно мгновение. Так что C всегда будет быстрее для таких операций.
Если код, использующий массив, в любом случае заполнит его перед чтением, то для Java это в основном бесполезное усилие. Но в случае, когда код читается первым, вы получаете предсказуемые результаты в Java, но непредсказуемые результаты в C.
источник
valgrind
, который покажет, где именно было использовано неинициализированное значение. Вы не можете использоватьvalgrind
код Java, потому что среда выполнения выполняет инициализацию, делаяvalgrind
проверки s бесполезными.Неопределенное поведение обеспечивает значительную оптимизацию, предоставляя широте компилятора возможность делать что-то странное или неожиданное (или даже нормальное) при определенных границах или других условиях.
Смотрите http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
источник
a + b
компилировать в нативнуюadd b a
инструкцию в любой ситуации, вместо того, чтобы потенциально требовать от компилятора имитировать какую-то другую форму целочисленной арифметики со знаком.HashSet
замечательна.<<
может быть трудный случай.x << y
оценивает какое-то допустимое значение типа,int32_t
но мы не будем говорить, какой». Это позволяет разработчикам использовать быстрое решение, но не действует как ложное предварительное условие, позволяющее оптимизировать стиль путешествий во времени, поскольку недетерминизм ограничен выводом этой одной операции - спецификация гарантирует, что память, переменные и т. Д. Не будут видны незаметно по оценке выражения. ...В первые дни C было много хаоса. Различные компиляторы относились к языку по-разному. Когда было интересно написать спецификацию для языка, эта спецификация должна была быть достаточно обратно совместимой с C, на который программисты полагались со своими компиляторами. Но некоторые из этих деталей непереносимы и не имеют общего смысла, например, предполагают конкретную последовательность или порядок данных. Таким образом, стандарт C сохраняет много деталей в виде неопределенного или заданного реализацией поведения, что оставляет большую гибкость разработчикам компиляторов. C ++ основан на C, а также имеет неопределенное поведение.
Java старался быть намного безопаснее и намного проще, чем C ++. Java определяет семантику языка в терминах виртуальной машины. Это оставляет мало места для неопределенного поведения, с другой стороны, предъявляет требования, которые могут быть затруднены для реализации Java (например, ссылки должны быть атомарными или работать как целые числа). Если Java поддерживает потенциально небезопасные операции, они обычно проверяются виртуальной машиной во время выполнения (например, некоторые приведения).
источник
this
null?», Проверяет некоторое время назад, на том основании, чтоthis
бытие былоnullptr
UB, и, следовательно, никогда не может произойти.)В языках JVM и .NET это легко:
Есть хорошие моменты для выбора, хотя:
Там, где предусмотрены аварийные люки, они возвращаются к полноценному неопределенному поведению. Но, по крайней мере, они обычно используются только на нескольких очень коротких отрезках, которые, таким образом, легче проверить вручную.
источник
unsafe
ключевое слово или атрибуты вSystem.Runtime.InteropServices
). Предоставляя этот материал нескольким программистам, которые знают, как отлаживать неуправляемые вещи, и, опять же, настолько мало, насколько это практически возможно, мы устраняем проблемы. Прошло более 10 лет с момента последнего небезопасного молотка, связанного с производительностью, но иногда это нужно делать, потому что другого решения буквально нет.Java и C # характеризуются доминирующим вендором, по крайней мере, на раннем этапе их разработки. (Sun и Microsoft соответственно). C и C ++ разные; у них было несколько конкурирующих реализаций с самого начала. Особенно C работал на экзотических аппаратных платформах. В результате произошли различия между реализациями. Комитеты ISO, которые стандартизировали C и C ++, могли бы согласовать большой общий знаменатель, но на краях, где реализации отличаются, стандарты оставляли место для реализации.
Это также объясняется тем, что выбор одного поведения может быть дорогостоящим для аппаратных архитектур, которые смещены в сторону другого выбора - порядковый номер является очевидным выбором.
источник
Настоящая причина сводится к фундаментальной разнице в намерениях между C и C ++, с одной стороны, и Java и C # (только для пары примеров), с другой. По историческим причинам большая часть обсуждения здесь говорит о C, а не C ++, но (как вы, вероятно, уже знаете) C ++ является довольно прямым потомком C, поэтому то, что он говорит о C, в равной степени относится и к C ++.
Хотя они в значительной степени забыты (а их существование иногда даже отрицается), самые первые версии UNIX были написаны на ассемблере. Большая часть (если не только) первоначальной цели C была переносом UNIX с языка ассемблера на язык более высокого уровня. Часть намерения состояла в том, чтобы написать как можно больше операционной системы на языке более высокого уровня - или смотреть на это с другой стороны, чтобы минимизировать количество, которое должно было быть написано на языке ассемблера.
Для этого C необходимо было обеспечить почти такой же уровень доступа к оборудованию, как и на языке ассемблера. PDP-11 (для одного примера) сопоставляет регистры ввода / вывода с конкретными адресами. Например, вы прочитали одну ячейку памяти, чтобы проверить, была ли нажата клавиша на системной консоли. В этом месте был установлен один бит, когда были данные, ожидающие чтения. Затем вы читаете байт из другого указанного местоположения, чтобы получить код ASCII нажатой клавиши.
Аналогично, если вы хотите распечатать некоторые данные, вы проверите другое указанное местоположение, а когда устройство вывода будет готово, вы запишите свои данные еще в одном указанном месте.
Для поддержки написания драйверов для таких устройств C позволил вам указать произвольное местоположение, используя некоторый целочисленный тип, преобразовать его в указатель и прочитать или записать это местоположение в памяти.
Конечно, это имеет довольно серьезную проблему: не у каждой машины на Земле есть память, идентичная PDP-11 начала 1970-х годов. Таким образом, когда вы берете это целое число, конвертируете его в указатель, а затем читаете или пишете через этот указатель, никто не может дать разумную гарантию того, что вы собираетесь получить. Просто для наглядного примера, чтение и запись могут отображаться в отдельные регистры аппаратного обеспечения, поэтому вы (в отличие от обычной памяти), если вы что-то пишете, затем пытаетесь прочитать это обратно, то, что вы читаете, может не соответствовать тому, что вы написали.
Я вижу несколько возможностей, которые оставляют:
Из них 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 ++), для выполнения своей работы.
источник
Авторы Стандарта C ожидали, что их читатели узнают что-то, что, по их мнению, было очевидным, и на что они ссылались в опубликованном Обосновании, но прямо не сказали: Комитету не нужно заказывать составителей компиляторов для удовлетворения потребностей своих клиентов, поскольку клиенты должны лучше, чем Комитет, знать, каковы их потребности. Если очевидно, что компиляторы для определенных видов платформ должны обрабатывать конструкцию определенным образом, никого не должно волновать, говорит ли Стандарт, что эта конструкция вызывает неопределенное поведение. Неспособность Стандарта предписывать, чтобы соответствующие компиляторы обрабатывали фрагмент кода с пользой, никоим образом не означает, что программисты должны быть готовы покупать компиляторы, которые этого не делают.
Этот подход к языковому дизайну очень хорошо работает в мире, где авторам компиляторов нужно продавать свои продукты платящим клиентам. Он полностью разваливается в мире, где авторы компиляторов изолированы от воздействия рынка. Сомнительно, чтобы когда-либо существовали надлежащие рыночные условия, чтобы управлять языком так, как они управляли языком, который стал популярным в 1990-х годах, и еще более сомнительно, что любой разработчик здравомыслящих языков захочет полагаться на такие рыночные условия.
источник
C ++ и c оба имеют описательные стандарты (в любом случае, версии ISO).
Они существуют только для объяснения того, как работают языки, и для предоставления единой ссылки о том, что это за язык. Как правило, производители компиляторов и разработчики библиотек руководят, и некоторые предложения включаются в основной стандарт ISO.
Java и C # (или Visual C #, что, я полагаю, вы имеете в виду) имеют предписывающие стандарты. Они сообщают вам, что на языке определенно опережает время, как он работает и что считается допустимым поведением.
Более того, Java на самом деле имеет «эталонную реализацию» в Open-JDK. (Я думаю, что Roslyn считается эталонной реализацией Visual C #, но не смог найти источник для этого.)
В случае Java, если в стандарте есть неопределенность, и Open-JDK делает это определенным образом. То, как это делает Open-JDK, является стандартом.
источник
Неопределенное поведение позволяет компилятору генерировать очень эффективный код на различных архитектурах. В ответе Эрика упоминается оптимизация, но она выходит за рамки этого.
Например, переполнения со знаком являются неопределенным поведением в Си. На практике ожидалось, что компилятор сгенерирует простой код операции добавления со знаком для выполнения ЦП, и поведение будет таким, как этот конкретный ЦП.
Это позволило C работать очень хорошо и создавать очень компактный код на большинстве архитектур. Если бы в стандарте было указано, что целые числа со знаком должны переполняться определенным образом, тогда процессорам, которые ведут себя по-разному, потребовалось бы гораздо больше генерации кода для простого добавления со знаком.
Это является причиной большей части неопределенного поведения в C, и поэтому такие вещи, как размер,
int
различаются в разных системах.Int
зависит от архитектуры и обычно выбирается как самый быстрый и самый эффективный тип данных, который больше, чем achar
.Когда C был новым, эти соображения были важны. Компьютеры были менее мощными, часто с ограниченной скоростью обработки и памятью. C использовался там, где производительность действительно имела значение, и разработчики должны были понимать, как компьютеры работают достаточно хорошо, чтобы знать, как на самом деле эти неопределенные поведения будут действовать в их конкретных системах.
Более поздние языки, такие как Java и C #, предпочитали устранять неопределенное поведение по сравнению с необработанной производительностью.
источник
В некотором смысле, у Java также есть это. Предположим, вы дали неверный компаратор для Arrays.sort. Он может бросить исключение, если его обнаружит. В противном случае он будет сортировать массив каким-либо образом, который не гарантированно будет конкретным.
Аналогично, если вы изменяете переменную из нескольких потоков, результаты также непредсказуемы.
C ++ просто пошел дальше, чтобы создавать неопределенные дополнительные ситуации (или, скорее, Java решил определить больше операций) и иметь имя для него.
источник
a
было бы неопределенным поведением, если бы вы могли извлечь из него 51 или 73, но если вы можете получить только 53 или 71, оно четко определено.