c++pointersreferencelanguage-lawyernull-pointer

Can pointer taken from reference ever be null in well-defined c++?


This question is motivated by uneven treatment of checking pointer value against nullptr by clang and gcc. For this they both emit a warning, but for a pointer taken by using address-of operator on an object they keep quiet.

I am pretty sure that such pointer should always be valid, because we have experienced bugs due to modern compiler removing such checks on c++ code from happy 90's where it did actually fire.

It bugs me why compilers keep quiet in general case. Is it somehow possible for the if to trigger, or is it just a design decision in both major compilers? Before I start writing patches or bugging compiler devs, I'd like to be sure I'm not missing something.

Toy example:

#include <iostream>
class A {
    void f(){
        if(!this) {
            std::cout << "This can't trigger, and compilers warn about it.";
        }
    }
};

void f(A& a){
    A* ptr = &a;
    if(ptr == nullptr) {
        std::cout << "Can this trigger? Because gcc and clang are silent.";
    }
}

Even though the question seems pretty dumb, I find it practical. If one does work with smelly code this optimization has fatal results, so a warning would be a really useful diagnostic.

To supplement the case. Both clang and gcc do know that check has constant evaluation, because even for clean code:

void g(A* a){
    A* ptr = a;
    if(ptr == nullptr) {
        std::cout << "Gee, can this trigger? Be cause gcc and clang are silent.";
    }
}

void g(A& a) {
    g(&a);
}

They generate two versions of g with if omitted in g(A& a) so both are able to determine and assume non-nullability for reference. gcc generates nice readable assembly:

f(A&):
        ret
.LC0:
        .string "Can this trigger? Be cause gcc and clang are silent."
g(A*):
        test    rdi, rdi
        je      .L5
        ret
.L5:
        mov     edx, 52
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        jmp     std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
g(A&):
        ret

As far as I understand assembly msvc /O2 and icc -fast leave the check in place.

EDIT: I've missed ! in A::f(), fixed it.


Solution

  • Can pointer taken from reference ever be null in well-defined c++?

    No. Standard quotes in this answer: Is null reference possible?

    Although, in particular case of taking the pointer using an overloaded operator& of a class type can return anything, including null.

    Is it somehow possible for the if to trigger?

    Not in A::f nor ::f. It is possible to trigger in g(A*) but not when called from g(A&).

    a warning would be a really useful diagnostic.

    GCC nor Clang are not smart enough to detect the mistake in that case as you've observed, but they do detect a simpler version of the same mistake:

    GCC

    warning: the compiler can assume that the address of 'a' will never be NULL [-Waddress]
         if(&a == nullptr) {
            ~~~^~~~~~~~~~
    warning: nonnull argument 'a' compared to NULL [-Wnonnull-compare]
         if(&a == nullptr) {
         ^~
    

    Clang

    warning: reference cannot be bound to dereferenced null pointer in well-defined C++ code; comparison may be assumed to always evaluate to false [-Wtautological-undefined-compare]
       if(&a == nullptr) {