Стоит ли неопределенное поведение? - PullRequest
25 голосов
/ 05 мая 2010

Много плохих вещей происходило и продолжает происходить (или нет, кто знает, что может случиться) из-за неопределенного поведения. Я понимаю, что это было введено, чтобы оставить некоторую простор для оптимизации компиляторами, а также, возможно, чтобы упростить перенос C ++ на разные платформы и архитектуры. Однако проблемы, вызванные неопределенным поведением, кажутся слишком большими, чтобы быть оправданными этими аргументами. Каковы другие аргументы в пользу неопределенного поведения? Если их нет, почему все еще существует неопределенное поведение?

Редактировать Чтобы добавить мотивацию к моему вопросу: из-за нескольких неудачных попыток с меньшим количеством хитроумных коллег по C ++ я привык делать свой код максимально безопасным. Утверждайте каждый аргумент, строгую константность и все в таком духе. Я стараюсь оставить как можно меньше места, чтобы использовать мой код неправильно, потому что опыт показывает, что, если есть лазейки, люди будут их использовать, а потом они будут звонить мне, если мой код плохой. Я считаю, что мой код должен быть максимально безопасным. Вот почему я не понимаю, почему существует неопределенное поведение. Может кто-нибудь дать мне пример неопределенного поведения, которое не может быть обнаружено во время выполнения или во время компиляции без значительных накладных расходов?

Ответы [ 11 ]

9 голосов
/ 05 мая 2010

Мое мнение о неопределенном поведении таково:

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

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

8 голосов
/ 05 мая 2010

Я думаю, что суть проблемы заключается в философии скорости C / C ++, прежде всего.

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

Указание того, как обращаться с UB, будет означать сначала его обнаружение, а затем, конечно, указание самой обработки. Однако обнаружение этого противоречит первой философии языка!

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

Я знаю девиз , добавьте больше оборудования к проблеме . У нас есть приложение, в котором я работаю:

  • ожидаемое время для ответа? Менее 100 мс с вызовами БД в середине (скажем, благодаря memcached).
  • количество транзакций в секунду? 1200 в среднем, пики на 1500 / 1700.

Он работает на 40 монстрах: 8 двухъядерных процессоров (2800 МГц) с 32 ГБ оперативной памяти. На этом этапе становится все труднее «быстрее» с большим количеством аппаратного обеспечения, поэтому нам нужен оптимизированный код и язык, который это позволяет (мы ограничивались добавлением ассемблерного кода).

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

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

  • не использовать непроверенные звонки
  • (для экспертов) не используйте непроверенные звонки
  • (для гуру) вы уверены, что вам действительно нужен непроверенный звонок здесь?

И все вдруг лучше :) 1033 *

5 голосов
/ 05 мая 2010

Основным источником неопределенного поведения являются указатели, и поэтому C и C ++ имеют много неопределенного поведения.

Рассмотрим этот код:

char * r = 0x012345ff;
std::cout << r;

Этот код выглядит очень плохо, но должен ли он выдавать ошибку? Что, если этот адрес действительно читабелен, то есть это значение, которое я каким-то образом получил (может быть, адрес устройства и т. Д.)?

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

Помимо этого: в целом C ++ был спроектирован с учетом «правила нулевых издержек» (см. Проектирование и развитие C ++ ), поэтому он не мог наложить бремя на реализацию проверок для угловых случаев и т. д. Следует всегда помнить, что этот язык был разработан и действительно используется не только на настольных компьютерах, но и во встроенных системах с ограниченными ресурсами.

5 голосов
/ 05 мая 2010

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

Что касается:

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

Реальная проблема мира:

int * p = new int;
// call loads of stuff which may create an alias to p called q
delete p;

// call more stuff, somewhere in which you do:
delete q;

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

4 голосов
/ 05 мая 2010

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

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

3 голосов
/ 05 мая 2010

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

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

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

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

Таким образом, я вижу, что хорошая программная инженерия требует дисциплины на всех языках, разница в том, что происходит, когда наша дисциплина терпит неудачу, и то, сколько нам платят другие языки (в производительности и занимаемой площади, а функции C ++ вам нравятся ) для страхования от этого. Если страховка, предоставленная другим языком, того стоит для вашего проекта, возьмите ее. Если за функции, предоставляемые C ++, стоит платить с риском неопределенного поведения, возьмите C ++. Я не думаю, что нужно много спорить, как если бы это было глобальное свойство, которое одинаково для всех, оправдывают ли преимущества C ++ затраты. Они оправданы в рамках технического задания для дизайна языка C ++, который заключается в том, что вы не платите за то, что не используете. Следовательно, правильные программы не должны выполняться медленнее, чтобы неправильные программы получали полезное сообщение об ошибке вместо UB, и большую часть времени поведение в необычных случаях (например, << 32 32-битного значения) не должно определяться (например, в результате 0), если это потребует явной проверки необычного случая на оборудовании, которое комитет хочет поддерживать C ++ «эффективно».

Посмотрите на другой пример: я не думаю, что преимущества производительности профессионального компилятора Intel C и C ++ оправдывают затраты на его покупку. Следовательно, я не купил это. Это не значит, что другие сделают те же вычисления, что и я, или что я всегда буду делать такие же вычисления в будущем.

2 голосов
/ 05 мая 2010

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

UB, с другой стороны, существует в тех случаях, когда трудно или невозможно разработать решение без значительных изменений в языке или больших отличий от C. Один пример взят со страницы , где BS говорит об этом is:

int a[10];
a[100] = 0; // range error
int* p = a;
// ...
p[100] = 0; // range error (unless we gave p a better value before that assignment)

Ошибка диапазона - UB. Это ошибка, но то, как конкретно платформа должна справляться с этим, не определено Стандартом, потому что Стандарт не может его определить. Каждая платформа отличается. Он не может быть спроектирован с ошибкой, потому что для этого потребуется включить автоматическую проверку диапазона на языке, что потребует существенного изменения набора функций языка. Ошибка p[100] = 0 для языка еще сложнее сгенерировать диагностику, во время компиляции или во время выполнения, потому что компилятор не может знать, на что действительно указывает p без поддержки во время выполнения.

2 голосов
/ 05 мая 2010

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

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

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

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

Я не говорю, что C ++ - плохой язык! У него просто разные цели, и я люблю работать с ним. У вас также есть большое сообщество, отличные инструменты и многое другое, такое как STL, Boost и QT. Но ваши сомнения также являются корнем, чтобы стать великим программистом C ++. Если вы хотите хорошо работать с C ++, это должно быть одной из ваших проблем. Я бы посоветовал вам прочитать предыдущие слайды, а также этот критик . Это очень поможет вам понять те времена, когда язык не делает то, что вы ожидаете.

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

1 голос
/ 05 мая 2010

Вот мой любимый: после того, как вы сделали delete для ненулевого указателя, используя его (не только разыменование, но также и castin и т. Д.), Это UB (см. этот вопрос) .

Как вы можете столкнуться с UB:

{
    char* pointer = new char[10];
    delete[] pointer;
    // some other code
    printf( "deleted %x\n", pointer );
}

Теперь на всех известных мне архитектурах код будет работать нормально. Обучение компилятору или среде выполнения для анализа таких ситуаций очень сложно и дорого. Не забывайте, что иногда это может быть миллионы строк кода между delete и использованием указателя. Установочные указатели на null сразу после delete могут быть дорогостоящими, поэтому это также не универсальное решение.

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

1 голос
/ 05 мая 2010

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

Не все устройства имеют концепцию защищенной памяти. Таким образом, вы не можете полагаться на то, что система защитит вас через segfault или подобное. Не все устройства имеют только постоянную память, поэтому нельзя сказать, что запись просто ничего не делает. Единственный другой вариант, о котором я мог подумать, - это требовать, чтобы приложение вызывало исключение [или отмену, или что-то еще] без помощи системы. Но в этом случае компилятор должен вставлять код перед каждой отдельной записью в память, чтобы проверить наличие нуля, если только он не может гарантировать, что указатель не изменился с момента записи в память списка. Это явно недопустимо.

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

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