34
$\begingroup$

Certain constructs or conditions in programming just are not allowed. Languages such as Java or Swift handle these by raising an error when encountered. C and C++ on the other hand say 'Anything could happen, might error, might work, might work some of the time.'

What are the advantages of leaving behavior undefined as opposed to raising an error on an invalid program?

$\endgroup$
0

9 Answers 9

33
$\begingroup$

This is a controversial topic

The C89 standard, section 3.16, defines:

undefined behavior: Behavior, upon use of a nonportable or erroneous program construct, or of erroneous data, or of indeterminately-valued objects, for which this International Standard imposes no requirements. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).

And the 1999 C Rationale explains why:

Undefined behavior gives the implementor license not to catch certain program errors that are difficult to diagnose. It also identifies areas of possible conforming language extension: the implementor may augment the language by providing a definition of the officially undefined behavior.

The C99 standard, section 3.4.3, words it slightly differently, turning the second sentence into an explanatory note:

undefined behavior behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements

NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).

EXAMPLE An example of undefined behavior is the behavior on integer overflow.

Note that in the second sentence, the word "permissible" was changed to the word "possible".

This has resulted in two schools of thought in the C and C++ community.

The first group, largely made of C programmers, argues that the second sentence in the C89 standard was normative: it described the set of permissible behaviours. So "undefined behaviour" is only "undefined" in the sense that the standard does not require which of the permissible behaviours an implementation may do.

The second group, largely made of "standards lawyers" and open source C compiler implementors, point out that, under ISO rules, moving the second sentence to an explanatory note and changing the word "permissible" to "possible" means that it is not normative. These are merely possible behaviours, but because the standard imposes no requirements, any behaviour is possible.

This is also known as a "nasal demon", because the compiler making demons fly out of your nose is also a possible behaviour.

This is a problem for many C programmers, since it meant that WG14 declared a lot of customary C usage to be undefined behaviour. Chris Lattner of LLVM put it this way: "huge bodies of C code are land mines just waiting to explode."

So, for example, if you ever write this:

i << 32

If i is a 32-bit integer, shifting it left by 32 is undefined behaviour. According to the second interpretation, any instance of this anywhere on an execution path renders the whole program semantically meaningless. And specifically, a compiler may assume that undefined behaviour can never happen and optimise your code accordingly.

This brings us to SPECint, the standard suite of integer benchmarks maintained by the Standard Performance Evaluation Corporation. This set of benchmarks is how C compiler vendors evaluate and market their code-generation and optimisation performance.

One of the benchmarks is 464.h264ref, which is a reference implementation of H.264/AVC video coding. It contains the following interestingly-written function:

int d[16]; 
int SATD (void) 
{ 
    int satd = 0, dd, k; 
    for (dd=d[k=0]; k<16; dd=d[++k]) { 
        satd += (dd < 0 ? -dd : dd); 
    } 
    return satd; 
} 

Please ignore the code quality for a moment. What I want you to notice is the for loop, specifically dd=d[++k]. On the last iteration of the loop, this reads one element past the end of the array. The value is never used, so the read is actually safe. Nonetheless, this is technically undefined behaviour.

Apparently nobody noticed until a pre-release version of GCC 4.8 optimised it. The compiler reasoned:

  • The out-of-bounds access of d cannot possibly happen.
  • Therefore k cannot be greater than 15 before the array access d[++k].
  • Therefore the loop test k<16 is always true.
  • Therefore this is an infinite loop.
  • Therefore all the other code in this function is dead code.

And the final generated code was this:

SATD: 
.L2: 
    jmp .L2  

It was fixed before GCC was released, and it now gives a warning. Let it also be a warning to you.

$\endgroup$
10
  • 6
    $\begingroup$ "the compiler making demons fly out of your nose is also a possible behaviour." -- Aw man, I much prefer my free GCC pizza. $\endgroup$
    – Bbrk24
    Commented May 19, 2023 at 2:06
  • 9
    $\begingroup$ Why is reading one element beyond the end of the array technically safe? If it were sitting on the edge of a memory page, it could access unmapped memory, or in less safe systems, I/O mapped memory or past the end of memory. $\endgroup$
    – prosfilaes
    Commented May 20, 2023 at 19:24
  • 4
    $\begingroup$ It's true, there are circumstances where it could be potentially unsafe. You'd have to be extremely unlucky. Nonetheless, this was an accepted C idiom at the time this code was written. $\endgroup$
    – Pseudonym
    Commented May 21, 2023 at 0:34
  • 2
    $\begingroup$ There's a difference between "this operation may do whatever the target machine does in this situation" and "this operation renders the whole program semantically meaningless". $\endgroup$
    – Pseudonym
    Commented Jul 10, 2023 at 14:36
  • 1
    $\begingroup$ The GCC bug is this one $\endgroup$
    – corvus_192
    Commented Jul 10, 2023 at 16:14
19
$\begingroup$

Performance

Checking for errors at runtime is quite expensive. Branching operators massively are slow instructions, especially since they prevent many optimizations. If every basic operator needed a sanity check it would massively add to the runtime of your program.

On the other hand, a lot of things that are "undefined behavior" are quite easily checked by the programmer. Languages like C and C++ choose to trust the programmer to check these things properly and therefore benefit from faster code.

Languages like Rust do do more checks by default but even then the checks can be disabled for developers who know for sure they would pass.

Flexibility of implementation

Languages can have multiple implementations, and multiple versions of the same implementation. Implementing some functionality in one version might be much harder than another, possibly because of platform constraints.

You could specify the behavior of one version and force other implementations to implement it but it would be slow, inconvenient, and might mean your language can't even run on some platforms.

Implementors also want freedom to update the implementation over time. Maybe a faster algorithm is discovered but it behaves slightly differently in situations where it shouldn't really be used anyways. (eg. sorting a array with a non-transitive comparison function)

Thus, just providing some constraints for how some functionality must be handled allows implementors to choose the best implementation without fear of breaking existing programs.

Optimization (Performance, again)

Compiled languages often have a optimizer. Optimizers massively increase performance but need to make assumptions about the code to work. CPUs nowadays are able to execute many instructions at once so long as they use different parts of the chip and a whole bunch of very complex rules are also followed. Compilers will rearrange your instruction so that multiple instructions can be run at once.

To do this, the compiler will build a graph of the dependencies of each instruction so it can guarantee nothing will be run before the data it depends on. However, the compiler needs to make assumptions to do this.

For example it needs to know none of the data it needs will be written to by another thread. It may assume that no pointers alias. It may also assume no integers overflows.

These things are very hard to check for a compiler but relatively easy to check for a programmer. Considering the huge performance benefit this is usually considered worth it.

$\endgroup$
0
14
$\begingroup$

Besides performance and ease of writing an implementation, there is one other reason for undefined behavior.

If some property is impossible to check for and cannot be defined because it's unpredictable by nature, you have to make it undefined.

The best example is data races and race conditions. You can't check for them in the compiler (without designing the language around it, like Rust), and they can have unpredictable results, so you must make their behavior undefined.

$\endgroup$
12
  • 5
    $\begingroup$ Even in rust race conditions are possible, even only in safe code $\endgroup$ Commented May 18, 2023 at 5:42
  • 2
    $\begingroup$ Many languages recognize the existence of race conditions, but offer some behavioral guarantees with regard to their effects. In Java, for example, if following the last sequencing barrier an int or an object reference that is written by one or more threads and read by one or more threads, each read may independently yield either the value held at the barrier, or any value that was written after it, and at the next sequencing barrier the object may receive any of the values that was written, but those are the only possible things that can happen. $\endgroup$
    – supercat
    Commented Jul 17, 2023 at 15:50
  • 2
    $\begingroup$ This allows things like Java's string hashCode function to safely cache its result without memory barriers. If one thread hashes a string and then another does so later, but there hasn't been any memory barriers between the operations, the latter thread might not see the cached result from the first and thus perform a redundant hash calculation, but the lack of memory barriers would have no consequence beyond the wasted effort of computing the hash again. $\endgroup$
    – supercat
    Commented Jul 17, 2023 at 15:52
  • 1
    $\begingroup$ Those limitations on the behavior of data races (which you do mention in your post) also limit the behavior of race conditions more generally. Because the program will always take one of some set of possibilities, you do not have undefined behavior- only nondeterminism. $\endgroup$
    – rpjohnst
    Commented Jul 19, 2023 at 0:00
  • 1
    $\begingroup$ @GavinD.Howard This is a common misunderstanding of Turing-completeness and Rice’s theorem, but it isn’t true. $\endgroup$
    – Alexis King
    Commented Jul 19, 2023 at 5:43
9
$\begingroup$

The premise of undefined behavior is assuming that erroneous conditions never happen. The responsibility of checking and avoiding such conditions is placed on the programmer and not on the implementation. While many languages check for exceptional conditions every time one may happen, C and C++ do not. This removes safety, because the underlying behavior of the undefined construct can be unpredictable, but improves performance and ease of writing an implementation, as less (redundant?) checks are being made by the implementation, because it is assumed that the programmer did their job.

If C were to require that dereferencing NULL yielded a runtime error, the compiler may have to spend time tracking which pointers can be NULL and/or insert NULL tests that manually crash the program at every point a pointer is dereferenced. Instead, the anything goes aspect of undefined behavior permits the compiler to assume these will never happen and omit checks. Because any possible unpredictable behavior result from omitting the test would be valid 'undefined behavior.'

$\endgroup$
3
  • 1
    $\begingroup$ Relatively few programs are run in circumstances where they can be guaranteed never to receive erroneous inputs, and implementaitons which assume all possible actions in response to invalid inputs would be equally acceptable shoudl be recognized as unsuitable for use outside such circumstances. $\endgroup$
    – supercat
    Commented Jul 11, 2023 at 4:10
  • 1
    $\begingroup$ Note also that according to the C Standard, the term "Undefined Behavior" is used to describe "non-portable or erroneous program constructs, or erroneous inputs". For many kinds of program, an assumption that no "non-portable" constructs would be executed would be patently absurd, as would be the notion that all possible behaviors to erroneous inputs would equally satisfy application requirements. The C Standard can't exercise jurisdiction over what will happen if a program uses fopen(name,"r") to read a file that some other program wrote in binary mode, but... $\endgroup$
    – supercat
    Commented Jul 17, 2023 at 15:55
  • 2
    $\begingroup$ ...the fact that a strictly conforming program might attempt to open a file in such a case can hardly be viewed as implying the program itself is "erroneous". If an environment would make it easy for an fopen implementation to return null, or else translate the file in a documented fashion, an implementation that does one of those things would be preferable to one that would arbitrarily corrupt the memory, but if the underlying OS would corrupt memory when attempting to perform such an action, a C implementation can't very well be required to prevent that. $\endgroup$
    – supercat
    Commented Jul 17, 2023 at 15:59
9
$\begingroup$

The point about UB is that the language – the compiler, or the interpreter – does not have to worry about extreme or edge cases. It's the programmer that has to worry about such situations. In other words, "Undefined Behaviour" takes a lot of worrying away from the language itself, and puts it back onto the shoulders of the user of the language. Whether that is a good idea or not is up for discussion.

Consider the following C function. Suppose this function is put into a separate file (separate "compilation unit"):

void f(int *x) {
    x[100] = 25;
}

This compiles without problems, even though at compile time it is not known what x points to. But the compiler is happy to oblige anyway: it takes the pointer x, it adds to it 100 times the size of an integer, and it attempts to write "25" into the resulting memory location. Whether this succeeds or not, and what the actual result is, depends on how this function is called.

It could be that (in the application that uses f) we have something like

int a[1000];
f(a);

in which case invoking f won't do anything unusual or undefined. But it also could be that the function is invoked as

int a[10];
f(a);

In this case we simply cannot say what the result of this function call is. It could attempt to write to memory which the current process doesn't have access to; this would result in an "Access violation" or "Segmentation fault" error, or something similar; possibly resulting in program termination. But it is also possible that this function will overwrite some other memory belonging to the same process – with completely unpredictable results!

And that's the whole point of "undefined behaviour". The language itself (the compiler or the interpreter) does not have to worry about special cases – for example it does not have to check that a pointer points to a valid memory area (that the current process has access to). This shifts a lot of responsibility from the language itself onto its users!

$\endgroup$
1
  • $\begingroup$ To make it defined behaviour, you would have to define what happens in the second case. Presumably the caller would tell f that indices 0 to 9 are valid for read/write and you are allowed to take the address at index 10, and f would check that and stop executing at the assignment with index 100. $\endgroup$
    – gnasher729
    Commented Dec 3, 2023 at 12:56
8
$\begingroup$

From the ISO C++ wiki's FAQ item, "Why are some things left undefined in C++?":

Because machines differ and because C left many things undefined. For details, including definitions of the terms “undefined”, “unspecified”, “implementation defined”, and “well-formed”; see the ISO C++ standard. Note that the meaning of those terms differ from their definition of the ISO C standard and from some common usage. You can get wonderfully confused discussions when people don’t realize that not everybody shares definitions.

This is a correct, if unsatisfactory, answer. Like C, C++ is meant to exploit hardware directly and efficiently. This implies that C++ must deal with hardware entities such as bits, bytes, words, addresses, integer computations, and floating-point computations the way they are on a given machine, rather than how we might like them to be.

The rest of that FAQ goes on to give examples of where UB can be useful for performance reasons, to relieve standard-implementation-tooling from having to try to diagnose (report errors for) ill-formed code that is difficult to diagnose due to the C++ compilation model, and sometimes just due to implementations making use of already standardized UB to perform certain purported optimizations that they are now unwilling to change.

If you're interested in the work that the C++ committee is doing on reducing the number of things it specifies as UB, check out Study Group 12.

Also related: https://en.cppreference.com/w/cpp/language/ub.

You can find some other "useful everyday examples" of UB in C++ at https://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html#optimizations, including UB from using uninitialized variables (where allowing uninitialized variables is useful for performance), signed integer overflow being UB being useful for certain compiler optimizations, etc.

Sometimes it's done just to reserve space for future changes to the types of code that are labelled UB. Titus Winters has done a talk on this subject, and his paper P0921, which is a user-facing document about "Rights the Standard Library Reserves for Itself" (in the form of certain kinds of UB, mostly to carve out room for itself to change in the future).

$\endgroup$
1
  • $\begingroup$ Note that there should be a distinction between "reserved for future versions of the Standard" versus "UB", in that the former should allow for the possibility of a programmer writing #if XXXX #define __future_identifier [[...form compatible with old-compiler, but not necessarily optimal...]] #endif to write code that will benefit from new compiler features if available, but still work on old compilers. $\endgroup$
    – supercat
    Commented Jul 11, 2023 at 4:18
4
$\begingroup$

The term "Undefined Behavior" is a catch-all used to refer to any of three kinds of situations without trying to distinguish among them:

  1. An implementation is called upon to process a program which, while non-portable, is correct on that platform.

  2. An implementation is called upon to render a program which is erroneous, rendering questions of portability irrelevant.

  3. An implementations is called upon to process a correct and portable program, but the input is erroneous in ways that cannot be reliably meaningfully handled via any standard means (e.g. using "r" mode with fopen to open a file that some other program has written using "wb" mode).

For an implementation to be able to flag an error in response to some erroneous construct, it would need to have some means of determining whether the construct was erroneous rather than correct but non-portable. In many cases, it would be impossible to specify language constructs in a manner that would flag all cases where they were used erroneously, without flagging cases where they were used in correct-but-non-portable fashion to accomplish tasks not provided for by the Standard.

$\endgroup$
3
$\begingroup$

Actively terminating the program may be the worst possible outcome anyway, when the broken run still gives some chances for it to accomplish the mission.

One of the cases would be a rocket controller. If the controller crashes completely, the rocket fails and this is the worst that can happen anyway. Otherwise in some cases a single error may be survivable, like in Ariane V88 crash where it happened in the rocket alignment task no longer important at the given stage of the flight.

Of course, the range of such cases is rather limited. A more typical consumer program would benefit more from printing diagnostics and terminating, making the bugs easier to detect.

$\endgroup$
3
  • $\begingroup$ The problem is that this "may" be the worst possible outcome; in another situation this might be the best outcome. How can the language and/or compiler know that? $\endgroup$
    – G. Sliepen
    Commented Jul 12, 2023 at 12:27
  • $\begingroup$ Some languages have compile time switches to enable or disable assertions. All testing is normally done with assertions enabled, but for production, depends. $\endgroup$ Commented Jul 12, 2023 at 12:32
  • $\begingroup$ On something like the Ariane, it may be possible in retrospect to determine that the craft would have behaved acceptably if the error had been ignored, but the time required to ascertain that almost certainly exceeded the time that would generally have been available to avoid having the rocket lose control and destroy something on the ground. $\endgroup$
    – supercat
    Commented Jul 22, 2023 at 19:14
2
$\begingroup$

Because there are no portable semantics for doing so, or specifying such semantics would unnecessarily constrain implementations.

Performance is oft-cited as the reason for undefined behavior but this tends to hide the real reason.

Undefined behavior comes from the fact that to specify everything in a programming language would be incredibly infeasible and would also not allow differentiation of implementations.

This is why some things are left undefined in programming language standards rather than them being explicitly spelled out. It both encourages a diversity of implementations that the language can support while letting those that want to target a specific superset do so by opting into certain features at the expense of loss of portability.

Signed integer overflow is a classic example. What do you get when you do

int i = INT_MAX + 1;

It certainly... is C code1, but what does it actually do. Well... it's not defined because there's no portable semantics for such an operation. Depending on the implementation, it may wraparound (as many programmers on two's complement machines expect), it may saturate (as on some DSPs), it may trap, or do any number of things. You could also conceive of an implementation that checked all additions it couldn't prove or aborted translation if it could prove such a thing (at least within some set of assumptions!).

Especially in languages that gained popularity before standardization (such as C (or languages that inherited such concerns from those languages, like C++), many compilers had and have differing ways of expressing certain idioms.

1. There are certainly some who would argue that this isn't C code at all, but it's a view that, for all intents and purposes, is an unnecessarily prescriptivist (and perhaps useless) view of what a C program is, one which is unsupported by the standard itself.
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .