50

This Stack Overflow post lists a fairly comprehensive list of situations where the C/C++ language specification declares as to be 'undefined behaviour'. However, I want to understand why other modern languages, like C# or Java, doesn't have the concept of 'undefined behavior'. Does it mean, the compiler designer can control all possible scenarios (C# and Java) or not (C and C++)?

11 Answers 11

72

Undefined behaviour is one of those things that were recognized as a very bad idea only in retrospect.

The first compilers were great achievements and jubilantly welcomed improvements over the alternative - machine language or assembly language programming. The problems with that were well-known, and high-level languages were invented specifically to solve those known problems. (The enthusiasm at the time was so great that HLLs were somtimes hailed as "the end of programming" - as if from now on we would only have to trivially write down what we wanted and the compiler would do all the real work.)

It wasn't until later that we realized the newer problems that came with the newer approach. Being remote from the actual machine that code runs on means there is more possibility of things silently not doing what we expected them to do. For instance, allocating a variable would typically leave the initial value undefined; this wasn't considered a problem, because you wouldn't allocate a variable if you didn't want to hold a value in it, right? Surely it wasn't too much to expect that professional programmers wouldn't forget to assign the initial value, was it?

It turned out that with the larger code bases and more complicated structures that became possible with more powerful programming systems, yes, many programmers would indeed commit such oversights from time to time, and the resulting undefined behaviour became a major problem. Even today, the majority of security leaks from tiny to horrible are the result of undefined behaviour in one form or another. (The reason is that usually, undefined behaviour is in fact very much defined by things on the next lower level on computing, and attackers who understand that level can use that wiggle room to make a program do not only unintended things, but exactly the things they intend.)

Since we recognised this, there has been a general drive to banish undefined behaviour from high-level languages, and Java was particularly thorough about this (which was comparatively easy since it was designed to run on its own specifically designed virtual machine anyway). Older languages like C can't easily be retrofitted like that without losing compatibility with the huge amount of existing code.

Edit: As pointed out, efficiency is another reason. Undefined behaviour means that compiler writers have a lot of leeway for exploiting the target architecture so that each implementation gets away with the fastest possible implementation of each feature. This was more important on yesterday's underpowered machines than with today, when programmer salary is often the bottleneck for software development.

  • 56
    I don’t think that a lot of people of C community would agree with this statement. If you would retrofit C and define undefined behavior (e.g. default-initialize everything, chose an order of evaluation for function parameter, etc), the large base of well-behaved code would continue to work perfectly well. Only code that would not be well defined today would be disrupted. On the other side, if you leave undefined as today, compilers would continue to be free to exploit new advances in CPU architectures and code optimisation. – Christophe Sep 21 at 15:11
  • 13
    The main part of the answer does not really sound convincing for me. I mean, it's basically impossible to write a function that safely adds two numbers (as in int32_t add(int32_t x, int32_t y)) in C++. The usual arguments around that one are related to efficiency, but often interspersed with some portability arguments (as in "Write once, run... on the platform where you wrote it ... and nowhere else ;-)"). Roughly, one argument could therefore be: Some things are undefined because you don't know whether you're on a 16bit microcontoller or an a 64bit server (a weak one, but still an argument) – Marco13 Sep 21 at 22:46
  • 12
    @Marco13 Agreed - and getting rid of the "undefined behaviour" issue by making something "defined behaviour, but not necessarily what the user wanted and with no warning when it happens" instead of "undefined behaviour" is just playing code-lawyer games IMO. – alephzero Sep 22 at 0:09
  • 9
    "Even today, the majority of security leaks from tiny to horrible are the result of undefined behaviour in one form or another." Citation needed. I thought most of them were XYZ injection now. – Joshua Sep 22 at 4:26
  • 34
    "Undefined behaviour is one of those things that were recognized as a very bad idea only in retrospect." That's your opinion. Many (myself included) do not share it. – Lightness Races with Monica Sep 22 at 18:17
103

Basically because the designers of Java and similar languages didn't want undefined behavior in their language. This was a trade off - allowing undefined behavior has the potential to improve performance, but the language designers prioritized safety and predictability higher.

For example, if you allocate an array in C, the data is undefined. In Java, all bytes must be initialized to 0 (or some other specified value). This means the runtime must pass over the array (an O(n) operation), while C can perform the allocation in an instant. So C will always be faster for such operations.

If the code using the array is going to populate it anyway before reading, this is basically wasted effort for Java. But in the case where the code read first, you get predictable results in Java but unpredictable results in C.

  • 19
    Excellent presentation of the HLL dilemma: safety and ease of use vs. performance. There is no silver bullet: there are use cases for each side. – Christophe Sep 21 at 15:15
  • 5
    @Christophe To be fair, there are much better approaches to a problem than letting UB go totally uncontested like C and C++. You could have a safe, managed language, with escape hatches into unsafe territory, for you to apply where beneficial. TBH, it'd be really nice to just be able to compile my C/C++ program with a flag that says "insert whatever expensive runtime machinery you need, I don't care, but just tell me about ALL of the UB that occurs." – Alexander Sep 22 at 0:42
  • 4
    A good example of a data structure that deliberately reads uninitialized locations is Briggs and Torczon's sparse set representation (e.g. see codingplayground.blogspot.com/2009/03/… ) Initialization of such a set is O(1) in C, but O(n) with Java's forced initialization. – Arch D. Robison Sep 22 at 3:33
  • 9
    While it is true that forcing initialization of data makes broken programs much more predictable, it does not guarantee intended behavior: If the algorithm expects to be reading meaningful data while erroneously reading the implicitly initialized zero, that is as much a bug as if it had read some garbage. With a C/C++ program such a bug would be visible by running the process under valgrind, which would show exactly where the uninitialized value was used. You can't use valgrind on java code because the runtime does the initialization, making valgrinds checks useless. – cmaster Sep 22 at 20:31
  • 5
    @cmaster Which is why the C# compiler doesn't allow you to read from uninitialized locals. No need for runtime checks, no need for initialization, just compile-time analysis. It's still a trade-off, though - there are some cases where you don't have a good way to handle branching around potentially unassigned locals. In practice, I haven't found any cases where this wasn't a bad design in the first place and better solved through rethinking the code to avoid the complicated branching (which is hard for humans to parse), but it's at least possible. – Luaan Sep 23 at 7:05
42

Undefined behavior enables significant optimization, by giving the compiler latitude to do something odd or unexpected (or even normal) at certain boundary or other conditions.

See http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

Use of an uninitialized variable: This is commonly known as source of problems in C programs and there are many tools to catch these: from compiler warnings to static and dynamic analyzers. This improves performance by not requiring that all variables be zero initialized when they come into scope (as Java does). For most scalar variables, this would cause little overhead, but stack arrays and malloc'd memory would incur a memset of the storage, which could be quite costly, particularly since the storage is usually completely overwritten.


Signed integer overflow: If arithmetic on an 'int' type (for example) overflows, the result is undefined. One example is that "INT_MAX+1" is not guaranteed to be INT_MIN. This behavior enables certain classes of optimizations that are important for some code. For example, knowing that INT_MAX+1 is undefined allows optimizing "X+1 > X" to "true". Knowing the multiplication "cannot" overflow (because doing so would be undefined) allows optimizing "X*2/2" to "X". While these may seem trivial, these sorts of things are commonly exposed by inlining and macro expansion. A more important optimization that this allows is for "<=" loops like this:

for (i = 0; i <= N; ++i) { ... }

In this loop, the compiler can assume that the loop will iterate exactly N+1 times if "i" is undefined on overflow, which allows a broad range of loop optimizations to kick in. On the other hand, if the variable is defined to wrap around on overflow, then the compiler must assume that the loop is possibly infinite (which happens if N is INT_MAX) - which then disables these important loop optimizations. This particularly affects 64-bit platforms since so much code uses "int" as induction variables.

  • 27
    Of course, the real reason why signed-integer overflow is undefined is that when C was developed, there were at least three different representations of signed integers in use (one's-complement, two's-complement, sign-magnitude, and perhaps offset binary), and each gives a different result for INT_MAX+1. Making overflow undefined permits a + b to be compiled to the native add b a instruction in every situation, rather than potentially requiring a compiler to simulate some other form of signed integer arithmetic. – Mark Sep 22 at 2:41
  • 2
    Allowing integer overflows to behave in loosely defined fashion allows significant optimizations in cases where all possible behaviors would meet application requirements. Most of those optimizations will be forfeit, however, if programmers are required to avoid integer overflows at all costs. – supercat Sep 22 at 14:20
  • 5
    @supercat Which is another reason why avoiding undefined behaviour is more common in more recent languages - programmer time is valued a lot more than CPU time. The kind of optimizations C is allowed to do thanks to UB are essentially pointless on modern desktop computers, and make reasoning about code much harder (not to mention the security implications). Even in performance critical code, you can benefit from high-level optimizations that would be somewhat harder (or even much harder) to do in C. I have my own software 3D renderer in C#, and being able to use e.g. a HashSet is wonderful. – Luaan Sep 23 at 7:10
  • 2
    @supercat: W.r.t._loosely defined_, the logical choice for integer overflow would be to require Implementation Defined Behavior. That is an existing concept, and it's not an undue burden on implementations. Most would get away with "it's 2's complement with wrap-around", I suspect. << might be the difficult case. – MSalters Sep 23 at 10:06
  • @MSalters There is an simple and well studied solution that is neither undefined behavior or implementation defined behavior: nondeterministic behavior. That is, you can say "x << y evaluates to some valid value of the type int32_t but we won't say which". This allows implementers to use the fast solution, but does not act as a false precondition allowing time-travel style optimizations because the nondeterminism is constrained to the output of this one operation - the spec guarantees that memory, volatile variables, etc are not visibly affected by the expression evaluation. ... – Mario Carneiro Sep 24 at 1:20
20

In C's early days, there was a lot of chaos. Different compilers treated the language differently. When there was interest to write a specification for the language, that specification would need to be fairly backwards-compatible with the C that programmers were relying on with their compilers. But some of those details are non-portable and do not make sense in general, for example assuming a particular endianess or data layout. The C standard therefore reserves a lot of details as undefined or implementation-specified behaviour, which leaves a lot of flexibility to compiler writers. C++ builds upon C and also features undefined behaviour.

Java tried to be a much safer and much simpler language than C++. Java defines the language semantics in terms of a thorough virtual machine. This leaves little space for undefined behaviour, on the other hand it makes requirements that can be difficult for a Java implementation to do (e.g. that reference assignments must be atomic, or how integers work). Where Java supports potentially unsafe operations, they are usually checked by the virtual machine at runtime (for example, some casts).

  • So are you saying, backwards compatibility is the only reason why C and C++ are not getting out of undefined behaviours? – Sisir Sep 21 at 12:41
  • 3
    It's definitely one of the bigger ones, @Sisir. Even among experienced programmers, you'd be surprised how much stuff that shouldn't break does break when a compiler changes how it handles undefined behaviour. (Case in point, there was a bit of chaos when GCC started optimising out "is this null?" checks a while back, on the grounds that this being nullptr is UB, and thus can never actually happen.) – Justin Time 2 Reinstate Monica Sep 21 at 23:11
  • 9
    @Sisir, another big one is speed. In C's early days, hardware was far more heterogeneous than it is today. By simply not specifying what happens when you add 1 to INT_MAX, you can let the compiler do whatever is fastest for the architecture (eg. a one's-complement system will produce -INT_MAX, while a two's-complement system will produce INT_MIN). Similarly, by not specifying what happens when you read past the end of an array, you can have a system with memory protection terminate the program, while one without won't need to implement expensive runtime bounds-checking. – Mark Sep 22 at 2:47
14

JVM and .NET languages have it easy:

  1. They don't have to be able to work directly with hardware.
  2. They only have to work with modern desktop and server systems or reasonably similar devices, or at least devices designed for them.
  3. They can impose garbage-collection for all memory, and forced initialization, thus getting pointer-safety.
  4. They got specified by a single actor who also provided the single definitive implementation.
  5. They get to choose safety over performance.

There are good points for the choices though:

  1. Systems programming is a whole different ballgame, and uncompromisingly optimising for application programming instead is reasonable.
  2. Admittedly, there is less exotic hardware all the time, but small embedded systems are here to stay.
  3. GC is ill-suited for non-fungible resources, and trades much more space for good performance. And most (but not nearly all) forced initializations can be optimized away.
  4. There are advantages to more competition, but committees mean compromise.
  5. All those bounds-checks do add up, even though most can be optimized away. Null pointer checks can mostly be done by trapping access for zero overhead thanks to virtual address space, though optimisation is still inhibited.

Where escape-hatches are provided, those invite full-blown undefined behavior back in. But at least they are generally only used in few very short stretches, which are thus easier to manually verify.

  • 3
    Indeed. I program in C# for my job. Every once in awhile I reach for one of the unsafe-hammers (unsafe keyword or attributes in System.Runtime.InteropServices). By keeping this stuff to the few programmers who know how to debug unmanaged stuff and again as little of it as practical, we keep issues down. It's been more than 10 years since the last performance-related unsafe-hammer but sometimes you gotta do it because there's literally no other solution. – Joshua Sep 22 at 4:32
  • 19
    I frequently work on a platform from analog devices where sizeof (char) == sizeof (short) == sizeof (int) == sizeof (float) == 1. It also does saturating addition (so INT_MAX+1 == INT_MAX), and the nice thing about C is that I can have a conforming compiler that generates reasonable code. If the language mandated say twos complement with wrap around then every addition would end up with a test and a branch, something of a non starter in a DSP focused part. This is a current production part. – Dan Mills Sep 22 at 14:08
  • 5
    @BenVoigt Some of us live in a world where a small computer is maybe 4k of code space, a fixed 8 level call/return stack, 64 bytes of RAM, a 1MHz clock and costs <$0.20 in quantity 1,000. A modern mobile phone is a small PC with pretty much unlimited storage for all intents and purposes, and can be pretty much treated as a PC. Not all the world is multicore and lacks hard realtime constraints. – Dan Mills Sep 22 at 23:17
  • 2
    @DanMills: Not talking about modern mobile phones here with Arm Cortex A processors, talking about "feature phones" circa 2002. Yes 192kB of SRAM is a lot more than 64 bytes (which is not "small" but "tiny"), but 192kB also hasn't been accurately called "modern" desktop or server for 30 years. Also these days 20 cents will get you an MSP430 with a lot more than 64 bytes of SRAM. – Ben Voigt Sep 22 at 23:37
  • 2
    @BenVoigt 192kB might not be a desktop in the last 30 years, but I can assure you that it is entirely sufficient to be serving web pages, which I would argue makes such a thing a server by the very definition of the word. Fact is that that is an entirely reasonable (generous, even) amount of ram for a LOT of embedded applications that often include configuration web servers. Sure, I probably am not running amazon on it, but I just might be running a fridge complete with IOT crapware on such a core (With time and space to spare). Don't nobody need interpreted or JIT languages for that! – Dan Mills Sep 23 at 16:02
8

Java and C# are characterized by a dominant vendor, at least early in their development. (Sun and Microsoft respectively). C and C++ are different; they've had multiple competing implementations from early on. C especially ran on exotic hardware platforms, too. As a result, there was variation between implementations. The ISO committees that standardized C and C++ could agree on a large common denominator, but at the edges where implementations differ the standards left room for the implementation.

This is also because choosing one behavior might be expensive on hardware architectures that are biased towards another choice - endianness is the obvious choice.

  • What does a “large common denominator” mean literally? Are you talking about subsets or supersets? Do you realy mean enough factors in common? Is this like the least common multiple or the greatest common factor? This is very confusing for us robots who don't speak street lingo, just maths. :) – tchrist Sep 23 at 2:31
  • @tchrist: The common behavior is a subset, but this subset is pretty abstract. In many areas left unspecified by the common standard, real implementations must make a choice. Now some of those choices are pretty clear and therefore implementation-defined, but others are more vague. Memory layout at runtime is an example: there has to be a choice, but it's not clear how you'd document it. – MSalters Sep 23 at 6:57
  • 2
    The original C was made by one guy. It already had plenty of UB, by design. Things certainly got worse as C became popular, but UB was there from the very beginning. Pascal and Smalltalk had far less UB and were developed at pretty much the same time. The main advantage C had was that it was extremely easy to port - all the portability issues were delegated to the application programmer :P I've even ported a simple C compiler to my (virtual) CPU; doing something like LISP or Smalltalk would have been far greater effort (though I did have a limited prototype for a .NET runtime :). – Luaan Sep 23 at 7:29
  • @Luaan: Would that be Kernighan or Ritchie? And no, it didn't have Undefined Behavior. I know, I have had the original AT&T stenciled compiler documentation on my desk. The implementation did what it did. There was no distinction between unspecified and undefined behavior. – MSalters Sep 23 at 10:00
  • 4
    @MSalters Ritchie was the first guy. Kernighan only joined (not much) later. Well, it didn't have "Undefined Behaviour", because that term didn't exist yet. But it did have the same behaviour that would today be called undefined. Since C didn't have a specification, even "unspecified" is a stretch :) It was just something the compiler didn't care about, and the details were up to application programmers. It wasn't designed to produce portable applications, only the compiler was meant to be easy to port. – Luaan Sep 23 at 11:00
6

The real reason comes down to a fundamental difference in intent between C and C++ on one hand, and Java and C# (for only a couple of examples) on the other. For historical reasons, much of the discussion here talks about C rather than C++, but (as you probably already know) C++ is a fairly direct descendant of C, so what it says about C applies equally to C++.

Although they're largely forgotten (and their existence sometimes even denied), the very first versions of UNIX were written in assembly language. Much of (if not solely) the original purpose of C was port UNIX from assembly language to a higher level language. Part of the intent was to write as much of the operating system as possible in a higher level language--or looking at it from the other direction, to minimize the amount that had to be written in assembly language.

To accomplish that, C needed to provide nearly the same level of access to the hardware as assembly language did. The PDP-11 (for one example) mapped I/O registers to specific addresses. For example, you'd read one memory location to check whether a key had been pressed on the system console. One bit was set in that location when there was data waiting to be read. You'd then read a byte from another specified location to retrieve the ASCII code of the key that had been pressed.

Likewise, if you wanted to print some data, you'd check another specified location, and when the output device was ready, you'd write your data yet another specified location.

To support writing drivers for such devices, C allowed you to specify an arbitrary location using some integer type, convert it to a pointer, and read or write that location in memory.

Of course, this has a pretty serious problem: not every machine on earth has its memory laid out identically to a PDP-11 from the early 1970's. So, when you take that integer, convert it to a pointer, and then read or write via that pointer, nobody can provide any reasonable guarantee about what you're going to get. Just for an obvious example, reading and writing may map to separate registers in the hardware, so you (contrary to normal memory) if you write something, then try to read it back, what you read may not match what you wrote.

I can see a few possibilities that leaves:

  1. Define an interface to all possible hardware--specify the absolute addresses of all the locations you might want to read or write to interact with hardware in any way.
  2. Prohibit that level of access, and decree that anybody who wants to do such things needs to use assembly language.
  3. Allow people to do that, but leave it up to them to read (for example) the manuals for the hardware they're targeting, and write the code to fit the hardware they're using.

Of these, 1 seems sufficiently preposterous that it's hardly worth further discussion. 2 is basically throwing away the basic intent of the language. That leaves the third option as essentially the only one they could reasonable consider at all.

Another point that comes up fairly frequently is the sizes of integer types. C takes the "position" that int should be the natural size suggested by the architecture. So, if I'm programming a 32-bit VAX, int should probably be 32 bits, but if I'm programming a 36-bit Univac, int should probably be 36 bits (and so on). It's probably not reasonable (and might not even be possible) to write an operating system for a 36-bit computer using only types that are guaranteed to be multiples of 8 bits in size. Maybe I'm just being superficial, but it seems to me that if I were writing an OS for a 36-bit machine, I'd probably want to use a language that supported a 36-bit type.

From a language viewpoint, this leads to still more undefined behavior. If I take the largest value that will fit into 32 bits, what will happen when I add 1? On typical 32-bit hardware, it's going to roll over (or possibly throw some sort of hardware fault). On the other hand, if it's running on 36-bit hardware, it'll just...add one. If the language is going to support writing operating systems, you can't guarantee either behavior--you just about have to allow both the sizes of types and the behavior of overflow to vary from one to another.

Java and C# can ignore all of that. They aren't intended to support writing operating systems. With them, you have a couple of choices. One is to make the hardware support what they demand--since they demand types that are 8, 16, 32 and 64 bits, just build hardware that supports those sizes. The other obvious possibility is for the language to only run on top of other software that provides the environment they want, regardless of what the underlying hardware might want.

In most cases, this isn't really an either/or choice. Rather, many implementations do a little of both. You normally run Java on a JVM running on an operating system. More often than not, the OS is written in C, and the JVM in C++. If the JVM is running on an ARM CPU, chances are pretty good that the CPU includes ARM's Jazelle extensions, to tailor the hardware more closely to Java's needs, so less needs to be done in software, and the Java code runs faster (or less slowly, anyway).

Summary

C and C++ have undefined behavior, because nobody's defined an acceptable alternative that allows them to do what they're intended to do. C# and Java take a different approach, but that approach fits poorly (if at all) with the goals of C and C++. In particular, neither seems to provide a reasonable way to write system software (such as an operating system) on most arbitrarily chosen hardware. Both typically depend on facilities provided by existing system software (usually written in C or C++) to do their jobs.

4

The authors of the C Standard expected their readers to recognize something they thought was obvious, and alluded to in their the published Rationale, but didn't say outright: the Committee shouldn't need to order compiler writers to meet their customers' needs, since the customers should know better than the Committee what their needs are. If it's obvious that compilers for certain kinds of plaforms are expected to process a construct a certain way, nobody should care whether the Standard says that construct invokes Undefined Behavior. The Standard's failure to mandate that conforming compilers process a piece of code usefully in no way implies that programmers should be willing to buy compilers that don't.

This approach to language design works very well in a world where compiler writers need to sell their wares to paying customers. It completely falls apart in a world where compiler writers are isolated from the effects of the marketplace. It's doubtful the proper market conditions will ever exist to steer a language the way they had steered the one that became popular in the 1990s, and even more doubtful that any sane language designer would want to rely upon such market conditions.

  • I feel that you have described something important here, but it escapes me. Could you clarify your answer? Especially the second paragraph: it says the conditions now and the conditions earlier are different, but I don't get it; what exactly changed? Also, the "way" is now different than earlier; maybe explain this too? – anatolyg Sep 22 at 16:24
  • 4
    Seems your campaign to replace all undefined behavior with unspecified behavior or something more constrained is still going strong. – Deduplicator Sep 22 at 16:56
  • 1
    @anatolyg: If you haven't already, read the published C Rationale document (type C99 Rationale in Google). Page 11 lines 23-29 talk about the "marketplace", and page 13 lines 5-8 talk about what is intended with regard to portability. How do you think a boss at a commercial compiler company would react if a compiler writer told programmers who complained that the optimizer broke code that every other compiler handled usefully that their code was "broken" because it performs actions not defined by the Standard, and refused to support it because that would promote the continued... – supercat Sep 23 at 5:02
  • 1
    ...use of such constructs? Such a viewpoint is readily apparent on the support boards of clang and gcc, and has served to impede the development of intrinsics that could facilitate optimization far more easily and safely than the broken language gcc and clang want to support. – supercat Sep 23 at 5:04
  • 1
    @supercat: You're wasting your breath complaining to the compiler vendors. Why not direct your concerns to the language committees? If they agree with you, an errata will be issued which you can use to beat the compiler teams over the head. And that process is much quicker than the development of a new version of the language. But if they disagree, you're at least going to get actual reasons, whereas the compiler writers are just going to repeat (over and over) "We didn't designate that code broken, that decision was made by the language committee and we follow their decision." – Ben Voigt Sep 23 at 21:23
3

C++ and c both have descriptive standards (the ISO versions, anyway).

Which only exist to explain how the languages work, and to provide a single reference about what the language is. Typically, compiler vendors, and library writers, lead the way and some suggestions get included in the main ISO standard.

Java and C# (or Visual C#, which I assume you mean) have prescriptive standards. They tell you what's in the language definitively ahead of time, how it works, and what's considered permitted behavior.

More important than that, Java actually has a "reference implementation" in Open-JDK. (I think Roslyn counts as the Visual C# reference implementation, but couldn't find a source for that.)

In Java's case, if there's any ambiguity in the standard, and Open-JDK does it a certain way. The way Open-JDK does it is the standard.

  • The situation is worse than that: I don't think the Committee has ever achieved consensus about whether it's supposed to be descriptive or prescriptive. – supercat Oct 24 at 22:16
1

Undefined behaviour allows the compiler to generate very efficient code on a variety of architecturs. Erik's answer mentions optimization, but it goes beyond that.

For example, signed overflows are undefined behaviour in C. In practice the compiler was expected to generate a simple signed addition opcode for the CPU to execute, and the behaviour would be whatever that particular CPU did.

That allowed C to perform very well and produce very compact code on most architectures. If the standard had specified that signed integers had to overflow in a certain way then CPUs which behaved differently would have needed a lot more code generating for a simple signed addition.

That's the reason for much of the undefined behaviour in C, and why things like the size of int vary between systems. Int is architecture dependent and generally selected to be the fastest, most efficient data type that is larger than a char.

Back when C was new these considerations were important. Computers were less powerful, often having limited processing speed and memory. C was used where performance really mattered, and developers were expected to understand how computers worked well enough to know what these undefined behaviours would actually be on their particular systems.

Later languages such as Java and C# preferred eliminating undefined behaviour over raw performance.

-5

In a sense, Java also have it. Suppose, you gave incorrect comparator to Arrays.sort. It can throw exception of it detects it. Otherwise it will sort an array in some way that is not guaranteed to be any particular.

Similarly if you modify variable from several threads results are also unpredictable.

C++ is just went further to make undefined more situations(or rather java decided to define more operations) and to have a name for it.

  • 4
    That's not undefined behavior of the sort we're talking about here. "Incorrect comparators" come in two types: ones that define a total ordering, and ones that don't. If you provide a comparator that consistently defines the relative ordering of items, the behavior is well-defined, it's just not the behavior that the programmer wanted. If you provide a comparator that isn't consistent about the relative ordering, the behavior is still well-defined: the sort function will thrown an exception (which also probably isn't the behavior the programmer wanted). – Mark Sep 22 at 2:57
  • 2
    As for modifying variables, race conditions generally aren't considered undefined behavior. I don't know the details of how Java handles assignments to shared data, but knowing the general philosophy of the language, I'm pretty sure it's required to be atomic. Simultaneously assigning 53 and 71 to a would be undefined behavior if you could get 51 or 73 out of it, but if you can only get 53 or 71, it's well-defined. – Mark Sep 22 at 3:01
  • @Mark With chunks of data larger than the native word size of the system (for example, a 32 bit variable on a 16-bit word size system), it is possible to have an architecture that requires storing each 16-bit portion separately. (SIMD is another potential such situation.) In that case, even a simple source-code-level assignment is not necessarily atomic unless special care is taken by the compiler to ensure that it is executed atomically. – a CVn Sep 23 at 8:29

protected by gnat Sep 23 at 13:38

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.