Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there valid "use cases" for Undefined Behaviour?

I have found a piece of code which has UB, and was told to leave it in the code, with a comment that states it is UB. Using MSVC2012 only.

The code itself has a raw array of Foo objects, then casts that array to char* with reinterpret_cast<char*> and then calls delete casted_array (like this, not delete[]) on it.

Like this:

Foo* foos = new Foo[500];

char* CastedFoos = reinterpret_cast<char*>(foos);

delete CastedFoos;

Per the Standard 5.3.5/3 this is clearly Undefined Behavior.

Apparently this code does what it does to avoid having to call destructors as an optimisation.

I wondered, is there actually places where leaving UB in the code could be considered valid?

Also, as far as I'm concerned, leaving the above in code is not smart, am I right?

like image 973
Tony The Lion Avatar asked Dec 11 '22 15:12

Tony The Lion


2 Answers

It depends entirely on your perspective.

Take an extreme example: in C++03, threads were undefined behavior. As soon as you had more than one thread, your program's behavior was no longer defined by the C++ standard.

And yet, most people would say threads are useful.

Of course, multithreading may have been UB according to the C++ standard, but individual compilers didn't treat it as undefined. They provided an additional guarantee that multithreading is going to work as you'd expect.

When talking about C++ in the abstract, UB has no uses whatsoever. How could it? You don't know what could or would happen.

But in specific applications, specific code compiled by specific compilers to run on specific operating systems, you may sometimes know that a piece of UB is (1) safe, and (2) ends up having some kind of beneficial effect.

like image 56
jalf Avatar answered Dec 31 '22 01:12

jalf


The C++ standard defines "undefined behaviour" as follows:

behavior for which this standard imposes no requirements

So if you want your code to be portable to different compilers and platforms, then your code should not depend on undefined behavior, because what the programs (that are produced by different compilers compiling your code) do in these cases may vary.

If you don't care about portability, then you should check if your compiler documents how it behaves under the circumstances of interest. If it doesn't document what it does (and it doesn't have to), beware that the compiler could change what it does without warning between different versions. Also note that its behaviour may be non-deterministic. So for example it could crash 1% of the time, which you may not notice in ad-hoc testing, but will come back and bite you later when it goes into production. So even if you are using one compiler, it may still be a bad idea to depend on undefined behavior.

With regard to your specific example, you can rewrite it to achieve the same effect (not calling destructor, but reclaiming memory) in a way that does not result in undefined behaviour. Allocate a std::aligned_storage to hold the Foo array, call placement new to construct the Foo array on the aligned_storage, then when you want to deallocate the array, deallocate the aligned_storage without calling placement delete.

Of course this is still a terrible design, may cause memory leaks or other problems depending on what Foo::~Foo() was supposed to do, but at least it isn't UB.

like image 24
Andrew Tomazos Avatar answered Dec 31 '22 01:12

Andrew Tomazos