Действительно ли неопределенное поведение стоит того?

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

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

24
задан Björn Pollex 5 May 2010 в 09:32
поделиться

11 ответов

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

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

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

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

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

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

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

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

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

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

И вдруг все стало лучше :)

8
ответ дан 28 November 2019 в 23:37
поделиться

Я так понимаю поведение undefined:

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

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

9
ответ дан 28 November 2019 в 23:37
поделиться

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

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

4
ответ дан 28 November 2019 в 23:37
поделиться

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

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

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

1
ответ дан 28 November 2019 в 23:37
поделиться

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

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

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

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

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?

6
ответ дан 28 November 2019 в 23:37
поделиться

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

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

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

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

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

Помимо этого: в целом C++ был разработан с учетом "правила нулевых накладных расходов" (см. The Design and Evolution of C++), поэтому он не может налагать на реализацию никакого бремени по проверке угловых случаев и т.п. Всегда следует помнить, что этот язык был разработан и действительно используется не только на настольных компьютерах, но и во встроенных системах с ограниченными ресурсами.

5
ответ дан 28 November 2019 в 23:37
поделиться

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

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

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

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

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

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

2
ответ дан 28 November 2019 в 23:37
поделиться

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

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

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

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

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

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

3
ответ дан 28 November 2019 в 23:37
поделиться

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

Как можно запустить UB:

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

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

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

1
ответ дан 28 November 2019 в 23:37
поделиться

Бывают случаи, когда неопределенное поведение полезно. Возьмем, к примеру, большой int.

union BitInt
{
 __int64 Целый;
 struct
 {
 int Upper;
 int Lower; // или может быть нижний верхний. Зависит от архитектуры
 } Детали;
};

В спецификации говорится, что если мы в последний раз читали или писали в Whole, то чтение/запись из Parts не определено.

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

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

0
ответ дан 28 November 2019 в 23:37
поделиться

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

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

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
ответ дан 28 November 2019 в 23:37
поделиться
Другие вопросы по тегам:

Похожие вопросы: