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

Поведение стоит undefined?

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

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

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

Ответ 2

Мое поведение undefined заключается в следующем:

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

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

Ответ 3

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

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

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

Вопрос реального мира:

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;

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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

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

Ответ 6

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

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

Ответ 7

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

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

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

Ответ 8

Составители и языки программирования - одна из моих любимых тем. Раньше я занимался некоторыми исследованиями, связанными с компиляторами, и я нашел много раз поведение undefined.

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

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

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

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

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

Ответ 9

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

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

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

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

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

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

Ответ 10

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


union BitInt
{
    __int64 Whole;
    struct
    {
        int Upper;
        int Lower; // or maybe it lower upper. Depends on architecture
    } Parts;
};

Спектр говорит, что если мы в последний раз читаем или пишем "Целый", то чтение/запись из "Части" undefined.

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

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

Ответ 11

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

Как вы можете работать в UB:

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

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

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