Related
Specifically in reference to: https://blogs.msdn.microsoft.com/oldnewthing/20140627-00/?p=633/
I'm a new C++ programmer and I'm currently learning about undefined behavior and its effects on a program. I was linked specifically to the above blog, which says that when undefined behavior occurs, anything can happen.
It mentions several times specifically that the compiler can allow anything to happen when undefined behavior occurs.
What specifically causes this to occur, and why does it happen?
Nothing "causes" this to occur. Undefined behaviour cannot "occur". There is no mystical force that descends upon your computer and suddenly makes it create black holes inside of cats.
That anything can happen when you run a program whose behaviour is undefined, is stated as fact by the C++ standard. It's a statement of leeway, a handy excuse used by compilers to make assumptions about your code so as to provide useful optimisations.
For example, if we say that dereferencing nullptr is undefined (which it is) then no compiler needs to ever check that a pointer is not nullptr: it can just assume that a dereferenced pointer will never be nullptr, and if it's not then any consequences are the programmer's problem.
Due to the astounding complexity of compilers, some of those consequences can be rather unexpected.
Of course it is not actually true that "anything can happen". Your computer has neither the necessary physical power nor the necessary legal authority to instantiate a black hole inside of a cat. But since C++ is an abstraction, it seems only fitting that we use abstractions to teach people not to write programs with undefined behaviour. If you program rigorously, assuming that "anything can happen" if your program has undefined behaviour, then you will not be surprised by said rather unexpected consequences, and you will not be tempted to try to "control" the outcome in any way.
From the point of view of the C and C++ Standards, the fact that some situation invokes "Undefined Behavior" means nothing more nor less than that the Standard imposes no requirements on what an implementation must do in that situation in order to be conforming. It does not imply any particular judgment as to whether implementations intended for purposes on any particular platforms should be expected to behave predictably, nor whether predictable behavior in such a situation might be required to make an implementation suitable for a such purposes on such platforms.
For some reason, some compiler writers have equated "the Standard does not require X" with "there is no need for implementations to do X", without any particular regard for the purposes to which their compilers would be put, and without regard for what behaviors might be necessary to fulfill those purposes. What caused Undefined Behavior to digress to the "anything can happen" was that compiler writers interpreted it not as an acknowledgment that there may be some combinations of platform and application field where the cost of ensuring predictable behavior would exceed the benefit, and compiler writers should exercise judgment as to when that is the case, but instead as an indication that the authors of the Standard had already exercised judgment that on all combinations of platform and application field, the cost of ensuring predictable behavior would outweigh the benefits, and there's no need for compiler writers to exercise judgment because the authors of the Standard have already done so.
Specifically in reference to: https://blogs.msdn.microsoft.com/oldnewthing/20140627-00/?p=633/
I'm a new C++ programmer and I'm currently learning about undefined behavior and its effects on a program. I was linked specifically to the above blog, which says that when undefined behavior occurs, anything can happen.
It mentions several times specifically that the compiler can allow anything to happen when undefined behavior occurs.
What specifically causes this to occur, and why does it happen?
Nothing "causes" this to occur. Undefined behaviour cannot "occur". There is no mystical force that descends upon your computer and suddenly makes it create black holes inside of cats.
That anything can happen when you run a program whose behaviour is undefined, is stated as fact by the C++ standard. It's a statement of leeway, a handy excuse used by compilers to make assumptions about your code so as to provide useful optimisations.
For example, if we say that dereferencing nullptr is undefined (which it is) then no compiler needs to ever check that a pointer is not nullptr: it can just assume that a dereferenced pointer will never be nullptr, and if it's not then any consequences are the programmer's problem.
Due to the astounding complexity of compilers, some of those consequences can be rather unexpected.
Of course it is not actually true that "anything can happen". Your computer has neither the necessary physical power nor the necessary legal authority to instantiate a black hole inside of a cat. But since C++ is an abstraction, it seems only fitting that we use abstractions to teach people not to write programs with undefined behaviour. If you program rigorously, assuming that "anything can happen" if your program has undefined behaviour, then you will not be surprised by said rather unexpected consequences, and you will not be tempted to try to "control" the outcome in any way.
From the point of view of the C and C++ Standards, the fact that some situation invokes "Undefined Behavior" means nothing more nor less than that the Standard imposes no requirements on what an implementation must do in that situation in order to be conforming. It does not imply any particular judgment as to whether implementations intended for purposes on any particular platforms should be expected to behave predictably, nor whether predictable behavior in such a situation might be required to make an implementation suitable for a such purposes on such platforms.
For some reason, some compiler writers have equated "the Standard does not require X" with "there is no need for implementations to do X", without any particular regard for the purposes to which their compilers would be put, and without regard for what behaviors might be necessary to fulfill those purposes. What caused Undefined Behavior to digress to the "anything can happen" was that compiler writers interpreted it not as an acknowledgment that there may be some combinations of platform and application field where the cost of ensuring predictable behavior would exceed the benefit, and compiler writers should exercise judgment as to when that is the case, but instead as an indication that the authors of the Standard had already exercised judgment that on all combinations of platform and application field, the cost of ensuring predictable behavior would outweigh the benefits, and there's no need for compiler writers to exercise judgment because the authors of the Standard have already done so.
Most of the conversations around undefined behavior (UB) talk about how there are some platforms that can do this, or some compilers do that.
What if you are only interested in one platform and only one compiler (same version) and you know you will be using them for years?
Nothing is changing but the code, and the UB is not implementation-defined.
Once the UB has manifested for that architecture and that compiler and you have tested, can't you assume that from then on whatever the compiler did with the UB the first time, it will do that every time?
Note: I know undefined behavior is very, very bad, but when I pointed out UB in code written by somebody in this situation, they asked this, and I didn't have anything better to say than, if you ever have to upgrade or port, all the UB will be very expensive to fix.
It seems there are different categories of Behavior:
Defined - This is behavior documented to work by the standards
Supported - This is behavior documented to be supported a.k.a
implementation defined
Extensions - This is a documented addition, support for low level
bit operations like popcount, branch hints, fall into this category
Constant - While not documented, these are behaviors that will
likely be consistent on a given platform things like endianness,
sizeof int while not portable are likely to not change
Reasonable - generally safe and usually legacy, casting from
unsigned to signed, using the low bit of a pointer as temp space
Dangerous - reading uninitialized or unallocated memory, returning
a temp variable, using memcopy on a non pod class
It would seem that Constant might be invariant within a patch version on one platform. The line between Reasonable and Dangerous seems to be moving more and more behavior towards Dangerous as compilers become more aggressive in their optimizations
OS changes, innocuous system changes (different hardware version!), or compiler changes can all cause previously "working" UB to not work.
But it is worse than that.
Sometimes a change to an unrelated compilation unit, or far away code in the same compilation unit, can cause previously "working" UB to not work; as an example, two inline functions or methods with different definitions but the same signature. One is silently discarded during linking; and completely innocuous code changes can change which one is discarded.
The code that is working in one context can suddenly stop working in the same compiler, OS and hardware when you use it in a different context. An example of this is violating strong aliasing; the compiled code might work when called at spot A, but when inlined (possibly at link-time!) the code can change meaning.
Your code, if part of a larger project, could conditionally call some 3rd party code (say, a shell extension that previews an image type in a file open dialog) that changes the state of some flags (floating point precision, locale, integer overflow flags, division by zero behavior, etc). Your code, which worked fine before, now exhibits completely different behavior.
Next, many kinds of undefined behavior are inherently non-deterministic. Accessing the contents of a pointer after it is freed (even writing to it) might be safe 99/100, but 1/100 the page was swapped out, or something else was written there before you got to it. Now you have memory corruption. It passes all your tests, but you lacked complete knowledge of what can go wrong.
By using undefined behavior, you commit yourself to a complete understanding of the C++ standard, everything your compiler can do in that situation, and every way the runtime environment can react. You have to audit the produced assembly, not the C++ source, possibly for the entire program, every time you build it! You also commit everyone who reads that code, or who modifies that code, to that level of knowledge.
It is sometimes still worth it.
Fastest Possible Delegates uses UB and knowledge about calling conventions to be a really fast non-owning std::function-like type.
Impossibly Fast Delegates competes. It is faster in some situations, slower in others, and is compliant with the C++ standard.
Using the UB might be worth it, for the performance boost. It is rare that you gain something other than performance (speed or memory usage) from such UB hackery.
Another example I've seen is when we had to register a callback with a poor C API that just took a function pointer. We'd create a function (compiled without optimization), copy it to another page, modify a pointer constant within that function, then mark that page as executable, allowing us to secretly pass a pointer along with the function pointer to the callback.
An alternative implementation would be to have some fixed size set of functions (10? 100? 1000? 1 million?) all of which look up a std::function in a global array and invoke it. This would put a limit on how many such callbacks we install at any one time, but practically was sufficient.
No, that's not safe. First of all, you will have to fix everything, not only the compiler version. I do not have particular examples, but I guess that a different (upgraded) OS, or even an upgraded processor might change UB results.
Moreover, even having a different data input to your program can change UB behavior. For example, an out-of-bound array access (at least without optimizations) usually depend on whatever is in the memory after the array.
UPD: see a great answer by Yakk for more discussion on this.
And a bigger problem is optimization and other compiler flags. UB may manifest itself in a different ways depending on optimization flags, and it's quite difficult to imagine somebody to use always the same optimization flags (at least you'll use different flags for debug and release).
UPD: just noticed that you did never mention fixing a compiler version, you only mentioned fixing a compiler itself. Then everything is even more unsafe: new compiler versions might definitely change UB behavior. From this series of blog posts:
The important and scary thing to realize is that just about any
optimization based on undefined behavior can start being triggered on
buggy code at any time in the future. Inlining, loop unrolling, memory
promotion and other optimizations will keep getting better, and a
significant part of their reason for existing is to expose secondary
optimizations like the ones above.
This is basically a question about a specific C++ implementation. "Can I assume that a specific behavior, undefined by the standard, will continue to be handled by ($CXX) on platform XYZ in the same way under circumstances UVW?"
I think you either should clarify by saying exactly what compiler and platform you are working with, and then consult their documentation to see if they make any guarantees, otherwise the question is fundamentally unanswerable.
The whole point of undefined behavior is that the C++ standard doesn't specify what happens, so if you are looking for some kind of guarantee from the standard that it's "ok" you aren't going to find it. If you are asking whether the "community at large" considers it safe, that's primarily opinion based.
Once the UB has manifested for that architecture and that compiler and you have tested, can't you assume that from then on whatever the compiler did with the UB the first time, it will do that every time?
Only if the compiler makers guarantee that you can do this, otherwise, no, it's wishful thinking.
Let me try to answer again in a slightly different way.
As we all know, in normal software engineering, and engineering at large, programmers / engineers are taught to do things according to a standard, the compiler writers / parts manufacturers produce parts / tools that meet a standard, and at the end you produce something where "under the assumptions of the standards, my engineering work shows that this product will work", and then you test it and ship it.
Suppose you had a crazy uncle jimbo and one day, he got all his tools out and a whole bunch of two by fours, and worked for weeks and made a makeshift roller coaster in your backyard. And then you run it, and sure enough it doesn't crash. And you even run it ten times, and it doesn't crash. Now jimbo is not an engineer, so this is not made according to standards. But if it didn't crash after even ten times, that means it's safe and you can start charging admission to the public, right?
To a large extent what's safe and what isn't is a sociological question. But if you want to just make it a simple question of "when can I reasonably assume that no one would get hurt by me charging admission, when I can't really assume anything about the product", this is how I would do it. Suppose I estimate that, if I start charging admission to the public, I'll run it for X years, and in that time, maybe 100,000 people will ride it. If it's basically a biased coin flip whether it breaks or not, then what I would want to see is something like, "this device has been run a million times with crash dummies, and it never crashed or showed hints of breaking." Then I could quite reasonably believe that if I start charging admission to the public, the odds that anyone will ever get hurt are quite low, even though there are no rigorous engineering standards involved. That would just be based on a general knowledge of statistics and mechanics.
In relation to your question, I would say, if you are shipping code with undefined behavior, which no one, either the standard, the compiler maker, or anyone else will support, that's basically "crazy uncle jimbo" engineering, and it's only "okay" if you do vastly increased amounts of testing to verify that it meets your needs, based on a general knowledge of statistics and computers.
What you are referring to is more likely implementation defined and not undefined behavior. The former is when the standard doesn't tell you what will happen but it should work the same if you are using the same compiler and the same platform. An example for this is assuming that an int is 4 bytes long. UB is something more serious. There the standard doesn't say anything. It is possible that for a given compiler and platform it works, but it is also possible that it works only in some of the cases.
An example is using uninitialized values. If you use an uninitialized bool in an if, you may get true or false, and it may happen that it is always what you want, but the code will break in several surprising ways.
Another example is dereferencing a null pointer. While it will probably result in a segfault in all cases, but the standard doesn't require the program to even produce the same results every time a program is run.
In summary, if you are doing something that is implementation defined, then you are safe if you are only developing to one platform and you tested that it works. If you are doing something that is undefined behavior, then you are probably not safe in any case. There may be that it works but nothing guarantees it.
Think about it a different way.
Undefined behavior is ALWAYS bad, and should never be used, because you never know what you will get.
However, you can temper that with
Behavior can be defined by parties other than just the language specification
Thus you should never rely on UB, ever, but you can find alternate sources which state that a certain behavior is DEFINED behavior for your compiler in your circumstances.
Yakk gave great examples regarding the fast delegate classes. In those cases, the author explicitly claims that they are engaging in undefined behavior, according to the spec. However, they then go to explain a business reason why the behavior is better defined than that. For example, they declare that the memory layout of a member function pointer is unlikely to change in Visual Studio because there would be rampant business costs due to incompatibilities which are distasteful to Microsoft. Thus they declare that the behavior is "de facto defined behavior."
Similar behavior can be seen in the typical linux implementation of pthreads (to be compiled by gcc). There are cases where they make assumptions about what optimizations a compiler is allowed to invoke in multithreaded scenarios. Those assumptions are stated plainly in comments in the sourcecode. How is this "de facto defined behavior?" Well, pthreads and gcc go kind of hand in hand. It would be considered unacceptable to add an optimization to gcc which broke pthreads, so nobody will ever do it.
However, you cannot make the same assumption. You may say "pthreads does it, so I should be able to as well." Then, someone makes an optimization, and updates gcc to work with it (perhaps using __sync calls instead of relying on volatile). Now pthreads keeps functioning... but your code doesn't anymore.
Also consider the case of MySQL (or was it Postgre?) where they found a buffer overflow error. The overflow had actually been caught in the code, but it did so using undefined behavior, so the latest gcc started optimizing the entire check out.
So, in all, look for an alternate source of defining the behavior, rather than using it while it is undefined. It is totally legit to find a reason why you know 1.0/0.0 equals NaN, rather than causing a floating point trap to occur. But never use that assumption without first proving that it is a valid definition of behavior for you and your compiler.
And please oh please oh please remember that we upgrade compilers every now and then.
Historically, C compilers have generally tended to act in somewhat-predictable fashion even when not required to do so by the Standard. On most platforms, for example, a comparison between a null pointer and a pointer to a dead object will simply report that they are not equal (useful if code wishes to safely assert that the pointer is null and trap if it isn't). The Standard does not require compilers to do these things, but historically compilers which could do them easily have done so.
Unfortunately, some compiler writers have gotten the idea that if such a comparison could not be reached while the pointer was validly non-null, the compiler should omit the assertion code. Worse, if it can also determine that certain input would cause the code to be reached with an invalid non-null pointer, it should assume that such input will never be received, and omit all code which would handle such input.
Hopefully such compiler behavior will turn out to be a short-lived fad. Supposedly, it's driven by a desire to "optimize" code, but for most applications robustness is more important than speed, and having compilers mess with code that would have limited the damage caused by errant inputs or errand program behavior is a recipe for disaster.
Until then, however, one must be very careful when using compilers to read the documentation carefully, since there's no guarantee that a compiler writer won't have decided that it was less important to support useful behaviors which, though widely supported, aren't mandated by the Standard (such as being able to safely check whether two arbitrary objects overlap), than to exploit every opportunity to eliminate code which the Standard doesn't require it to execute.
Undefined behavior can be altered by things such as the ambient temperature, which causes rotating hard disk latencies to change, which causes thread scheduling to change, which in turn changes the contents of the random garbage that's getting evaluated.
In short, not safe unless the compiler or the OS specifies the behavior (since the language standard didn't).
There is a fundamental problem with undefined behavior of any kind: It is diagnosed by sanitizers and optimizers. A compiler can silently change behavior corresponding to those from one version to another (e.g. by expanding its repertoire), and suddenly you'll have some untraceable error in your program. This should be avoided.
There is undefined behavior that is made "defined" by your particular implementation, though. A left shift by a negative amount of bits can be defined by your machine, and it would be safe to use it there, as breaking changes of documented features occur quite rarely. One more common example is strict aliasing: GCC can disable this restriction with -fno-strict-aliasing.
While I agree with the answers that say that it's not safe even if you don't target multiple platforms, every rule can have exceptions.
I would like to present two examples where I'm confident that allowing undefined / implementation-defined behavior was the right choice.
A single-shot program. It's not a program which is intended to be used by anyone, but it's a small and quickly written program created to calculate or generate something now. In such a case a "quick and dirty" solution can be the right choice, for example, if I know the endianness of my system and I don't want to bother with writing a code which works with the other endianness. For example, I only needed it to perform a mathematical proof to know if I'll be able to use a specific formula in my other, user-oriented program or not.
Very small embedded devices. The cheapest microcontrollers have memory measured in a few hundred bytes. If you develop a small toy with blinking LEDs or a musical postcard, etc, every penny counts, because it will be produced in the millions with a very low profit per unit. Neither the processor nor the code ever changes, and if you have to use a different processor for the next generation of your product, you will probably have to rewrite your code anyway. A good example of an undefined behavior in this case is that there are microcontrollers which guarantee a value of zero (or 255) for every memory location at power-up. In this case you can skip the initialization of your variables. If your microcontroller has only 256 bytes of memory, this can make a difference between a program which fits into the memory and a code which doesn't.
Anyone who disagrees with point 2, please imagine what would happen if you told something like this to your boss:
"I know the hardware costs only $ 0.40 and we plan selling it for $ 0.50. However, the program with 40 lines of code I've written for it only works for this very specific type of processor, so if in the distant future we ever change to a different processor, the code will not be usable and I'll have to throw it out and write a new one. A standard-conforming program which works for every type of processor will not fit into our $ 0.40 processor. Therefore I request to use a processor which costs $ 0.60, because I refuse to write a program which is not portable."
"Software that doesn't change, isn't being used."
If you are doing something unusual with pointers, there's probably a way to use casts to define what you want. Because of their nature, they will not be "whatever the compiler did with the UB the first time".
For example, when you refer to memory pointed at by an uninitialize pointer, you get a random address that is different every time you run the program.
Undefined behavior generally means you are doing something tricky, and you would be better off doing the task another way.
For instance, this is undefined:
printf("%d %d", ++i, ++i);
It's hard to know what the intent would even be here, and should be re-thought.
Changing the code without breaking it requires reading and understanding the current code. Relying on undefined behavior hurts readability: If I can't look it up, how am I supposed to know what the code does?
While portability of the program might not be an issue, portability of the programmers might be. If you need to hire someone to maintain the program, you'll want to be able to look simply for a '<language x> developer with experience in <application domain>' that fits well into your team rather than having to find a capable '<language x> developer with experience in <application domain> knowing (or willing to learn) all the undefined behavior intrinsics of version x.y.z on platform foo when used in combination with bar while having baz on the furbleblawup'.
Nothing is changing but the code, and the UB is not implementation-defined.
Changing the code is sufficient to trigger different behavior from the optimizer with respect to undefined behavior and so code that may have worked can easily break due to seemingly minor changes that expose more optimization opportunities. For example a change that allows a function to be inlined, this is covered well in What Every C Programmer Should Know About Undefined Behavior #2/3 which says:
While this is intentionally a simple and contrived example, this sort of thing happens all the time with inlining: inlining a function often exposes a number of secondary optimization opportunities. This means that if the optimizer decides to inline a function, a variety of local optimizations can kick in, which change the behavior of the code. This is both perfectly valid according to the standard, and important for performance in practice.
Compiler vendors have become very aggressive with optimizations around undefined behavior and upgrades can expose previously unexploited code:
The important and scary thing to realize is that just about any optimization based on undefined behavior can start being triggered on buggy code at any time in the future. Inlining, loop unrolling, memory promotion and other optimizations will keep getting better, and a significant part of their reason for existing is to expose secondary optimizations like the ones above.
I read through several very good answers about undefined behaviour and sequence points (e.g. Undefined behavior and sequence points) and I understand, that
int i = 1;
a = i + i++; //this is undefined behaviour
is undefined code, according to the C++ standard. But what is the deeper reasoning behind it being undefined behaviour? Wouldn't it be enough to make it unspecified behaviour?
The normal argument is, that by having few sequence points, C++ compilers can optimize better for different architectures, but wouldn't leaving it unspecified allow those optimizations as well? In
a = foo(bar(1), bar(2)); //this is unspecified behaviour
the compiler can also optimize, and it is not undefined behaviour. In the first example it seems clear, that a is either 2 or 3, so the semantics seems to be clear to me. I hope there is a reasoning, why some things are unspecified, and others are undefined.
Not all of those optimizations. Itanium, for example, could perform both the add and increment in parallel, and it might cough up, say, a hardware exception for trying to do something like this.
But this is a completely micro-optimization, writing the compiler to take advantage of that was extremely difficult, and it's an extremely rare architecture that can do it (none existed at the time, IIRC, it was mostly hypothetical). So the reality is that as of 2012, there is no reason for it not to be well-defined behaviour, and indeed, C++11 made more of these situations well-defined.
From the viewpoint of C++, I think the answer is incredibly simple: it was made undefined behavior because C had made it undefined behavior long before, and there was essentially no potential gain from changing that.
That points to what I'd guess was really more the intended question: why did C make this undefined behavior?
I don't think that has quite as simple of an answer. One possibility is simple caution -- knowledge that by the time the C standard was being written, C had already been implemented, deployed and used on lots of machines. A fair number of machines back then seemed like a lot of code I still see: something originally designed only as a personal experiment, that worked well enough that it ended up designated as "production", without even a token attempt at fixing anything by the most egregious problems. As such, even if nobody knew of hardware this would break, nobody could be really sure such hardware didn't exist either, so it was safest to just call it UB, and be done with it.
Another possibility is that it went a bit beyond simple caution. Even though we can feel fairly safe with modern hardware, there may have been hardware at the time that people really knew would have major problems with this, and (especially if vendors associated with that hardware were represented on the committee) allowing C to run on that hardware was considered important.
Yet another possibility would be that even though nobody knew of (or even feared the possibility of) some existing implementation that this could break, they foresaw the future possibility of something it would break, so undefined behavior was seen as a way of future proofing the language to at least some limited degree.
A final possibility is that whoever was writing that part of the standard moved on to other things as soon as they came up with a set of rules that seemed acceptable, even though they could have come up with other rules that at least some might have liked better.
If I had to guess, I'd say it was probably a combination of the third and fourth possibilities I've given -- the committee was aware of developments in parallel computing without knowing how it would work out in the end, so for whomever wrote this, maximizing latitude on the part of the implementation seemed like the easiest/simplest route to gaining consensus so they could finish it and move on to bigger and better things.
There's a huge difference between undefined behavior and unspecified behavior. Unspecified behavior is well-formed (i.e., legal) but the standard leaves the compiler vendor some latitude as to implementation. Undefined behavior is an atrocity that appears to be syntactically correct. The primary reason for deeming behavior to be "undefined" rather than flat-out illegal (something the compiler must reject) is that sometimes that undefined behavior can be very hard to diagnose.
a = foo(bar(1), bar(2)); // this is unspecified behaviour
The two function calls can be made in any order, but they remain two different function calls. The machine instructions are not allowed to overlap even if the calls are inlined. In reality, they do overlap quite a bit, but the optimizer is restricted to produce code that behaves strictly as-if the function calls were separate.
a = i + i++; // this is undefined behaviour
With a scalar i, there is no requirement for separation: the cpu instructions that fetch from i, that add, and that postincrement, mix freely, and the optimizer is allowed to pretend it doesn't know that i on the left and i on the right are the same thing. There's no telling what kind of broken assembly it can produce when this precondition is violated. Thus, undefined.
MIPS 1 was a reasonable implementation with Load Delay slots. Executing a load would not be instant. The result was only visible after the next instruction was started. For compilers, this was no big deal. Just put an unrelated instruction in the next slot.
Of course, the compiler had to know what "unrelated" was. With the C rule against concurrent modifications of a single varibale, the compiler had far more choice in finding an instruction that had to be unrelated. If two operations appeared in a single statement, they have to operate on different variables and therefore be unrelated.
There'a a handful of situations that the C++ standard attributes as undefined behavior. For example if I allocate with new[], then try to free with delete (not delete[]) that's undefined behavior - anything can happen - it might work, it might crash nastily, it might corrupt something silently and plant a timed problem.
It's so problematic to explain this anything can happen part to newbies. They start "proving" that "this works" (because it really works on the C++ implementation they use) and ask "what could possibly be wrong with this"? What concise explanation could I give that would motivate them to just not write such code?
Undefined means explicitly unreliable. Software should be reliable. You shouldn't have to say much else.
A frozen pond is a good example of an undefined walking surface. Just because you make it across once doesn't mean you should add the shortcut to your paper route, especially if you're planning for the four seasons.
Two possibilities come to my mind:
You could ask them "just because you can drive on the motorway the opposite direction at midnight and survive, would you do it regularly?"
The more involved solution might be to set up a different compiler / run environment to show them how it fails spectacularly under different circumstances.
"Congratulations, you've defined the behavior that compiler has for that operation. I'll expect the report on the behavior that the other 200 compilers that exist in the world exhibit to be on my desk by 10 AM tomorrow. Don't disappoint me now, your future looks promising!"
Simply quote from the standard. If they can't accept that, they aren't C++ programmers. Would Christians deny the bible? ;-)
1.9 Program execution
The semantic descriptions in this International Standard define a parameterized nondeterministic abstract machine. [...]
Certain aspects and operations of the abstract machine are described in this International Standard as implementation-defined (for example, sizeof(int)). These constitute the parameters of the abstract machine. Each implementation shall include documentation describing its characteristics and behavior in these respects. [...]
Certain other aspects and operations of the abstract machine are described in this International Standard as unspecified (for example, order of evaluation of arguments to a function). Where possible, this International Standard defines a set of allowable behaviors. These define the nondeterministic aspects of the abstract machine. [...]
Certain other operations are described in this International Standard as undefined (for example, the effect of dereferencing the null pointer). [ Note: this International Standard imposes no requirements on the behavior of programs that contain undefined behavior. —end note ]
You can't get any clearer than that.
I'd explain that if they didn't write the code correctly, their next performance review would not be a happy one. That's sufficient "motivation" for most people.
Let them try their way until their code will crash during test. Then the words won't be needed.
The thing is that newbies (we've all been there) have some amount of ego and self-confidence. It's okay. In fact, you couldn't be a programmer if you didn't. It's important to educate them but no less important to support them and don't cut their start in the journey by undermining their trust in themselves. Just be polite but prove your position with facts not with words. Only facts and evidence will work.
John Woods:
In short, you can't use sizeof() on a structure whose elements haven't been
defined, and if you do, demons may fly out of your nose.
"Demons may fly out of your nose" simply must be part of the vocabulary of every programmer.
More to the point, talk about portability. Explain how programs frequently have to be ported to different OSes, let alone different compilers. In the real world, the ports are usually done by people other than the original programmers. Some of these ports are even to embedded devices, where there can be enormous costs of discovering that the compiler decided differently from your assumption.
Turn the person into a pointer. Tell them that they are a pointer to a class human and you are invoking the function 'RemoveCoat'. When they are pointing at a person and saying 'RemoveCoat' all is fine. If the person does not have a coat, no worries - we check for that, all RemoveCoat really does is remove the top layer of clothing (with decency checks).
Now what happens if they are pointing somewhere random and they say RemoveCoat - if they are pointing at a wall then the paint might peel off, if they are pointing at a tree the bark might come off, dogs might shave themselves, the USS Enterprise might lower its shields at a critical moment etc!
There is no way of working out what might happen the behaviour has not been defined for that situation - this is called undefined behaviour and must be avoided.
Quietly override new, new[], delete and delete[] and see how long it takes him to notice ;)
Failing that ... just tell him he is wrong and point him towards the C++ spec. Oh yeah .. and next time be more careful when employing people to make sure you avoid a-holes!
One would be...
"This" usage is not part of the language. If we would say that in this case the compiler must generate code that crashes, then it would be a feature, some kind of requirement for the compiler's manufacturer. The writers of the standard did not wanted to give unnecessary work on "features" that are not supported. They decided not to make any behavioral requirements in such cases.
I like this quote:
Undefined behavior: it may corrupt your files, format your disk or send hate mail to
your boss.
I don't know who to attribute this to (maybe it's from Effective C++)?
C++ is not really a language for dilletantes, and simply listing out some rules and making them obey without question will make for some terrible programmers; most of the stupidest things I see people say are probably related to this kind of blind rules following/lawyering.
On the other hand if they know the destructors won't get called, and possibly some other problems, then they will take care to avoid it. And more importantly, have some chance to debug it if they ever do it by accident, and also to have some chance to realize how dangerous many of the features of C++ can be.
Since there's many things to worry about, no single course or book is ever going to make someone master C++ or probably even become that good with it.
Just show them Valgrind.
Compile and run this program:
#include <iostream>
class A {
public:
A() { std::cout << "hi" << std::endl; }
~A() { std::cout << "bye" << std::endl; }
};
int main() {
A* a1 = new A[10];
delete a1;
A* a2 = new A[10];
delete[] a2;
}
At least when using GCC, it shows that the destructor only gets called for one of the elements when doing single delete.
About single delete on POD arrays. Point them to a C++ FAQ or have them run their code through cppcheck.
One point not yet mentioned about undefined behavior is that if performing some operation would result in undefined behavior, a standards-conforming implementation could legitimately, perhaps in an effort to be 'helpful' or improve efficiency, generate code which would fail if such an operation were attempted. For example, one can imagine a multi-processor architecture in which any memory location may be locked, and attempting to access a locked location (except to unlock it) will stall until such time as the location in question was unlocked. If the locking and unlocking were very cheap (plausible if they're implemented in hardware) such an architecture could be handy in some multi-threading scenarios, since implementing x++ as (atomically read and lock x; add one to read value; atomically unlock and write x) would ensure that if two threads both performed x++ simultaneously, the result would be to add two to x. Provided programs are written to avoid undefined behavior, such an architecture might ease the design of reliable multi-threaded code without requiring big clunky memory barriers. Unfortunately, a statement like *x++ = *y++; could cause deadlock if x and y were both references to the same storage location and the compiler attempted to pipeline the code as t1 = read-and-lock x; t2 = read-and-lock y; read t3=*t1; write *t2=t3; t1++; t2++; unlock-and-write x=t1; write-and-unlock y=t2;. While the compiler could avoid deadlock by refraining from interleaving the various operations, doing so might impede efficiency.
Turn on malloc_debug and delete an array of objects with destructors. freeing a pointer inside the block should fail. Call them all together and demonstrate this.
You'll need to think of other examples to build your credibility until they understand that they are newbies and there's a lot to know about C++.
Tell them about standards and how tools are developed to comply with the standards. Anything outside the standard might or might not work, which is UB.
Just because their program appears to work is a guarantee of nothing; the compiler could generate code that happens to work (how do you even define "work" when the correct behavior is undefined?) on weekdays but formats your disk on weekends. Did they read the source code to their compiler? Examine their disassembled output?
Or remind them just because it happens to "work" today is no guarantee of it working when you upgrade your compiler version. Tell them to have fun finding whatever subtle bugs creep up from that.
And really, why not? They should be providing a justifiable argument to use undefined behavior, not the other way around. What reason is there to use delete instead of delete[] other than laziness? (Okay, there's std::auto_ptr. But if you're using std::auto_ptr with a new[]-allocated array, you probably ought to be using a std::vector anyway.)
Both the C and C++ Standards use the term "Undefined Behavior" to refer to situations in which it may be useful for different implementations to process constructs in differing, incompatible, fashions, some of which will behave predictably but some of which may not. Both use the same terminology to describe UB, and while I don't know of any published Rationale for C++ Standards, the Rationale for the C Standard says:
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."
Note that many actions which were classified as Undefined Behavior by the C Standard were considered fully defined on many if not all implementations, but the authors of the Standard wanted to give implementors targeting unusual platforms or application fields the ability to deviate from the normal behaviors if doing so would benefit their customers. Such freedom was not intended to invite arbitrary and capricious deviations from precedent that make it harder for programmers to quickly and easily do what needed to be done.
Unfortunately, many programmers who use gcc and clang don't understand their needs as well as the maintainers of those compilers, who recognize that that since the Standard avoids mandating anything that would impair the efficiency of applications that will never receive maliciously-crafted inputs, or will only run in contexts where even malicious programs would be unable to damage anything, that implies that there's no need for any implementations to allow programmers to easily and efficiently write programs that are suitable for use in other contexts.