
Long ago, in a far-away galaxy, the MFC library was widely used, in which a number of classes had methods comparing this with zero. Like that:
class CWindow { HWND handle; HWND GetSafeHandle() const { return this == 0 ? 0 : handle; } };
“It does not make sense,” the reader will argue. Even as “has”: this code “allows” to call the GetSafeHandle () method via the null CWindow * pointer. This technique is used from time to time in different projects. Consider why this is actually a bad idea.
One has to start with the fact that, according to the C ++ Standard (follows from 5.2.5 / 3 of ISO / IEC 14882: 2003 (E)), calling any non-static method of any class through a null pointer leads to undefined behavior. However, in a number of implementations, this code may well work:
class Class { public: void DontAccessMembers() { ::Sleep(0); } }; int main() { Class* object = 0; object->DontAccessMembers(); }
This is due to the fact that during the operation of the method there are no attempts to access the members of the class, and no late binding is used to call the method. The compiler knows which particular method of which particular class to call, and simply adds a call to this method. In this case, this is passed as a parameter. The effect is the same as if the method were static:
class Class { public: static void DontAccessMembers(Class* currentObject) { ::Sleep(0); } }; int main() { Class* object = 0; Class::DontAccessMembers(object); }
If the method were called virtually, late binding would be necessary, which is usually implemented via a pointer to a virtual method table at the beginning of a particular object. In this case, even to find out which method you need to call, you need to access the contents of the object, and in the case of a null pointer, this will most likely lead to a program crash.
But we know that our method will never be called virtually, right? In general, this code has been working there for some years.
')
The problem is that the compiler can use undefined behavior for optimization. For example:
int divideBy = …; whatever = 3 / divideBy; if( divideBy == 0 ) {
The above code performs integer division by divideBy. Integer division by zero leads to undefined behavior (usually to crash the program). So, we can assume that the variable divideBy is not equal to zero, and at the compilation stage to exclude the check and optimize the code accordingly.
Similarly, the compiler can optimize and code comparing this with zero. In accordance with the Standard, this cannot be zero, respectively, the checks and the corresponding branches of the code can be eliminated, and this will significantly affect the code depending on the comparison of this with zero. The compiler has the full right to “break” (in fact, break it) the code CWindow :: GetSafeHandle () and generate machine code in which there is no comparison, but the class field is always read.
So far, even the newest versions of common compilers (you can check with the
GCC Explorer service) do not perform such optimizations, so while “everything works”, right?
First, you will be very unhappy when you switch to another compiler or another version of the same compiler, and you spend a lot of time to find out what about, now there is such an optimization. Therefore, the code above is intolerable.
Secondly,
class FirstBase { int firstBaseData; }; class SecondBase { public: void Method() { if( this == 0 ) { printf( "this == 0"); } else { printf( "this != 0 (value: %p)", this ); } } }; class Composed1 : public FirstBase, public SecondBase { }; int main() { Composed1* object = 0; object->Method(); }
Well, when compiling with Visual C ++ 9, the this pointer at the input to the method is 0x00000004, because the initially null pointer is adjusted to indicate the beginning of the sub-object of the corresponding class.
And if you change the order of the base classes
class Composed2 : public SecondBase, public FirstBase { }; int main() { Composed2* object = 0; object->Method(); }
under the same conditions, this will be zero, because the beginning of the sub-object coincides with the beginning of the object in which it is included. It turns out a wonderful class, whose method works only under the condition of the "correct" use of this class in composite objects. Happy debugging, the Darwin Award hasn't been this close in a long time.
It is easy to see that in the case of the class Composed1, the implicit conversion of a pointer to an object to a pointer to a sub-object works “incorrectly” - for a null pointer to an object, the conversion gives a non-zero pointer to a sub-object. Usually, when implementing a conversion of the same meaning, the compiler adds a check of a pointer to equality to zero. For example, compiling such a code with undefined behavior (the Composed1 class is the same as above):
SecondBase* object = reinterpret_cast<Composed1*>( rand() ); object->Method();
in Visual C ++ 9 it gives the following machine code:
SecondBase * object = reinterpret_cast <Composed1 *> (rand ());
010C1000 call dword ptr [__imp__rand (10C209Ch)]
010C1006 test eax, eax
010C1008 je wmain + 0Fh (10C100Fh)
010C100A add eax, 4
object-> Method ();
010C100D jne wmain + 20h (10C1020h)
010C100F push offset string "this == 0" (10C20F4h)
010C1014 call dword ptr [__imp__printf (10C20A4h)]
010C101A add esp, 4
In this machine code, the second instruction is a comparison of a pointer to an object with zero; if the pointer is equal to zero, control does not pass through the add eax, 4 instruction, which shifts the pointer. Here the implicit conversion is implemented with a check, although it was also possible to use the subsequent method call through the pointer and consider the pointer non-zero.
In the first case (calling the subobject's class method directly through a pointer to a class object), the equality of the pointer to zero also corresponds to undefined behavior, and the test is not added here. If, when reading a paragraph about optimizing a code with calling a method and then checking a pointer to zero, you thought it was nonsense and fantasy, it is in vain, here is a case described above in which such optimization has already been applied.
Relying on a non-static method call through a null pointer is a bad idea. If you need the ability to execute a method for a null pointer, you need to make the method static and explicitly pass the pointer to the object as a parameter.
Dmitry Mescheryakov,
product department for developers