понедельник, августа 29, 2005

Разница между unspecified behavior и undefined behavior

В Стандарте языка C++ в основном требуется вполне определенное поведение системы, реализующей язык. Например, если x и y типа int и их сумма попадает в диапазон -INT_MAX...+INT_MAX, результат операции сложения x+y должен быть равен их арифметической сумме.
Но есть случаи, в которых делаются некоторые допущения по поведению. Случаи unspecified behavior и undefined behavior перепутать легче всего.

Unspecified behavior. Неспецифицированное поведение. Система может реализовать любое из поведений (по большей части для случаев unspecified behavior, Стандарт предлагает несколько поведений на выбор), не должна говорить какое именно реализовано и даже не обязательно, чтобы она вела себя в таких ситуациях одинаково внутри одной программы или функции. При этом предполагается, что на вход была подана правильная (well-formed) программа, соответсвующая синтаксическим, семантическим и прочим правилам языка.
Например: все аргументы функции должны быть вычислены до вызова функции, но они могут быть вычислены в любом порядке. Система не должна выбирать какой-то определенный порядок и не должна указывать по каким правилам она этот порядок выбирает, чтобы его можно было предугадать.

Undefined behavior. Неопределенное поведение. Это поведение может возникать только как реакция на неправильную программу. Далеко не все неправильные конструкции могут вызвать такое поведение, большинство ошибок требуют вполне определенную диагностику.
Undefined behavior означает, что стандарт не накладывает каких-либо ограничений. Может случиться все, что угодно.
Например: разыменование указателя, который равен NULL, может дать 0, или любое произвольное значение, или остановку программы, или сигнал какого-либо вида, или исключение, или реализация может послать email вашему боссу с рассказом о том, какой вы неаккуратный программист. Или все вместе. Или что-то еще.

По большому счету получается, что unspecified behavior означает "как будет работать толком неизвестно, но все будет хорошо". Если оно случается, то это, как правило, нормально и не стоит по его поводу беспокоиться. А undefined behavior означает "как будет работать толком неизвестно, но все будет плохо". И надо от него срочно избавляться, если для его присутствия в программе нет неких загадочных причин.

Это не все поведения, есть еще, но в отличие от двух предыдущих по их названиям можно ясно себе представить, что они означают:

Implementation-defined behavior. Поведение, зависящее от реализации. Похоже на unspecified behavior, но в документации к системе должно быть указано, какое именно поведение реализовано. И на это поведение можно полагаться.
Например: результат x%y, где x и y целые, а y отрицательное, может быть как положительным, там и отрицательным, но в документации к системе должно быть написано каким именно он будет.

Locale-specific behavior. Поведение, зависящее от локали. Причем речь тут идет не только о языке. Стандарт говорит о "национальных обычаях, культуре и языке". Поведение должно быть документировано.
Например: должна ли функция islower возвратить true для символов, не входящих в 26 маленьких латинских букв.

Ссылки по теме:
1996 С++ Drawft Standarts - Definitions
Unspecified versus undefined behavior

Technorati tag:

8 коммент.:

Анонимный комментирует...

Про аргументы в функции - дурацкий пример.
Есть же calling convention* - в С - __stdcall, __cdecl, __fastcall (параметры помещаются в стек справа налево), была __pascal (теперь в С это устаревшая директива, и для вызова, например, экспортируемых функций из Delphi-dll нужно в этой самой dll функции как stdcall объявлять), где слева направо. Понимать ли это как "вычисляются в момент помещения в стек"? Видимо, да.

Поэтому (с точки зрения практики дурацкие) примеры типа
MyFunc( ++i, --i, i ); в С или аналог в Паскале (если calling convention по умолчанию, и явно не задана __stdcall, которая поместит их в "С" - порядке) будут по-разному работать.

Но вот пример с printf( "%d, %d, %d", ++i, --i, i ) и аналогами уже всегда будет (должен быть, точнее) __cdecl (чтобы там ни пытались экспериментаторы задавать), так как функции с переменным числом параметров всегда __cdecl - соответственно справа налево.

*на самом деле они и другими вещами отличаются, но тут это неважно.

PS
Насчет примера с shadow-map, то, что весной высылалось с исходниками is-shadowmap-src называлось, возможно, будет на TNT работать, там есть пример, как карты строить без depth-texture, иначе - не знаю.

Ivan Sagalaev комментирует...

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

Alena комментирует...

> Про аргументы в функции - дурацкий пример.
Кхм, кхм, насколько мне известно, именно этот пример приведен в Стандарте. Не в драфте, на который я ссылки даю, а в полном.

>Есть же calling convention
Да, но они не прописаны в стандарте. С точки зрения языка С++ и языка C этих конструкций не существует, даже несмотря на то, что компиляторы их поддерживают.
AFAIK, эти параметры впервые использовала Microsoft, а потом за ней потянулись остальные. Но все равно, код, использующий эти параметры, будет non-portable с точки зрения языка C++.

>Насчет примера с shadow-map, то, что весной высылалось с исходниками is-shadowmap-src

Угу, я потом еще на него в сетке набредала. Увы, там нет самозатенения, и на ландшафт тень либо целиком ляжет, либо вообще не ляжет... Я все-таки думаю сделать shadow volumes.

Анонимный комментирует...

Фактически это означает, что компилятор волен вычислить сначала все аргументы в случайном порядке...
А оператор "запятая" разве не означает последовательного вычисления? (в общем и в данном случае с параметрами ф-ии)
Да, но они не прописаны в стандарте.
Подобные вещи (как передаются, через регистр, через стек, в каком порядке, кто чистит стек и т.д.) пишутся скорее в описании к конкретной архитектуре (ABI - application binary interface) или документации к компилятору.
А как вычисляются - это уже про ,

код, использующий эти параметры, будет non-portable с точки зрения языка C++
Ну и что. А с "практической" точки зрения может оказаться вполне portable (если известно, что собираться будет 2-3 компиляторами, и они одинаково эти конвенции понимают).
К тому же это единственный способ (AFAIK) вызвать экспортируемые функции из Pascal библиотеки (а "стандард" тут бы сказал упс).
Или стандартные try/catch - их может просто не быть на какой-нибудь платформе - могут оказаться non-portable (и что, вместо программы "стандарт" запускать что ли).

Ivan Sagalaev комментирует...

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

Alena комментирует...

>А оператор "запятая" разве не означает последовательного вычисления? (в общем и в данном случае с параметрами ф-ии)

Как уже сказал Маньяк, оператор запятая и запятая, разделяющая параметры функции - это совсем разные запятые. Оператор запятая действительно означает последовательное вычисление слева направо.

>Или стандартные try/catch - их может просто не быть на какой-нибудь платформе

На платформе? В смысле, не реализовано компилятором? Это про какой-то реальный компилятор речь или просто предположение?

>и что, вместо программы "стандарт" запускать что ли

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

Анонимный комментирует...

Про "запятую" ясно.

На платформе? В смысле, не реализовано компилятором? Это про какой-то реальный компилятор речь или просто предположение?
Microsoft evc3 (embedded visual tools 3, Microsoft Windows CE);
любой компилятор С++ (gcc хотя бы), но под Symbian OS 6.0 - 8.0s (поиск "Symbian OS C++" легко находит официальный документ, хоть и не стандарт, конечно, почему так, а не иначе, и что делать);
кстати, туда же отсутствие stl/stlport.
Наверняка, найдутся и другие примеры.
Просто судить (о чьих-либо программах, исходном коде) только с точки зрения стандарта, по-моему, так же однобоко, как и "проверять правильность конструкций языка компилянием в своем любимом компиляторе".


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

Ну, просто подумалось, что с практической точки зрения на переносимость гораздо больше влияет использование специфичных библиотек, API, каких-то особенностей операционных систем, чем использование/неиспользование какого-нибудь "__fastcall" и т.д. В крайнем случае, "обернув" тот же "fastcall" в макрос и/или #define __fastcall
где не нужно, вполне себе можно жить.

Анонимный комментирует...

>Неспецифицированное поведение<

Абальдеть можно от произношения такой фразы. А чем Вам "Неуточняемое поведение" не нравится?