Could this
be null?
- Posted by Michał ‘mina86’ Nazarewicz on 13th of April 2025
- Share on Bluesky
- Cite
In my previous post, I mentioned an ancient C++ technique of using ((X*)0)->f()
syntax to simulate static methods. It ‘works’ by considering things from a machine code point of view, where a non-virtual method call is the same as a function call with an additional this
argument. In general, a well-behaving obj->method()
call is compiled into method(obj)
. With the assumption this is true, one might construct the following code:
struct Value { int safe_get() { return this ? value : -1; } int value; }; void print(Value *val) { printf("value = %d", val->safe_get()); if (val == nullptr) puts("val is null"); }
Will it work as expected though? A naïve programmer might assume this behaves the same as:
struct Value { int value; }; int Value_safe_get(Value *self) { return self ? self->value : -1; } void print(Value *val) { printf("value = %d", Value_safe_get(val)); if (val == nullptr) puts("val is null"); }
With the understanding from the previous post that the compiler treats undefined behaviour (UB) as something that cannot happen, we can analyse how the compiler is likely to optimise the program.
Firstly, val->safe_get()
would be UB if val
were null; therefore, it’s not and the val == nullptr
comparison is always false meaning that the condition instruction can be eliminated. Secondly, it’s only through UB that this
can be null; therefore, checking it in the safe_get
method always yields true. As such, the method can be optimised to a simple read of the value
field. Putting this together, a conforming compiler can transform the code into:
struct Value { int value; }; void print(Value *val) { printf("value = %d", val->value); }
And yes, that’s what GCC does.
Linux
This issue was encountered in Linux. At one point, the Universal TUN/TAP device driver contained the following code:
static unsigned int tun_chr_poll(struct file *file, poll_table * wait) { struct tun_file *tfile = file->private_data; struct tun_struct *tun = __tun_get(tfile); struct sock *sk = tun->sk; ⟵ line 5 unsigned int mask = 0; if (!tun) ⟵ line 8 return POLLERR; /* … */ }
The tun->sk
expression on line 5 is undefined if tun
is null. The compiler can thus assume that it is non-null. That means the !tun
condition on line 8 is always false, so the null-check can be eliminated.1
This bug has since been fixed, but issues like this prompted kernel developers to adopt the -fno-delete-null-pointer-checks
build flag, which forbears the compiler from omitting apparently useless null checks. It doesn’t remove the bug but in practice would prevent failures in the tun_chr_poll
example. As such, the flag acts as an additional defence against coding mistakes.
Windows
Microsoft has more of a gung-ho approach to the issue. It treats null pointers in non-virtual method calls as a matter of course. For example, the CWnd::
method ‘returns m_hWnd
, or null if the this
pointer is null.’ The method is implemented in the same way as the safe_get
method at the top of this article:
_AFXWIN_INLINE HWND CWnd::GetSafeHwnd() const { return this == NULL ? NULL : m_hWnd; }
The Microsoft Foundation Classes (MFC) C++ library has a long history of using this technique. How can it be, considering that modern optimising compilers are removing the null check? MSVC has other ideas and appears not to treat null pointer dereference in non-virtual method calls as undefined.
This may partially be because of backwards compatibility. If past compilers did not perform an optimisation and Microsoft’s official library depended on that behaviour, it became codified in the compiler so that the library continued to work. (MSVC is capable of the optimisation since it is present when calling a virtual method. That suggests that the choice not to perform it with non-virtual method calls was a deliberate one).
When undefined behaviour is defined
This brings us to another corner case of undefined behaviour. Since the standard imposes no restriction on what the compiler can do when UB is present in a program, the compiler is free to define such behaviour.
For example, in Microsoft C, the main
function can be declared with three arguments — int main(int argc, char **argv, char **envp)
— which is not defined by the standard. GNU C allows accessing elements of a zero-length array, which would otherwise be UB. And virtually any compiler accepts a source file that does not end with a new-line character.2
And then there are things that can be customised via flags like the aforementioned option preserving null checks. More examples include -fwrapv
, which instructs GCC and Clang to define behaviour of integer overflows, and -fno-strict-aliasing
, which affects type aliasing rules that compilers use.
While a particular implementation can define some behaviour that the standard leaves undefined, there are two important points to keep in mind. Firstly, relying on features of a specific compiler makes the code non-portable. Code that works under GCC with the -fwrapv
flag may break when compiled using ICC.
Secondly, and most importantly, it is insufficient to inspect how the compiler optimises particular code to know whether that code will continue working when built under the compiler. Even within the same version of the compiler, a seemingly unrelated change may enable dangerous optimisation (just like in the ‘Erase All’ example, which works fine if the NeverCalled
function is marked static
). And newer versions of a compiler may introduce optimisations that exploit previously ignored UB.
Conclusion
For maximum portability, one must eliminate all undefined behaviour. Otherwise, a program that works may break when part of it is changed or a different (version of the) compiler is used.
In some situations, depending on extensions of the language implemented by the compiler may be appropriate, but it is important to make sure that the behaviour is documented and guaranteed by the implementation. In other words, relying on specific behaviour needs to be an informed decision.