5

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.

curiousguy
  • 8,038
  • 2
  • 40
  • 58
luk32
  • 15,812
  • 38
  • 62
  • 5
    Pretty much a dupe of [Is null reference possible?](//stackoverflow.com/q/4364536), isn't it? – Baum mit Augen Jun 25 '19 at 14:53
  • 2
    Most likely it's not worth bothering about. There is no possible way to have `if(!this)` pass. `if(ptr == nullptr)` on the other hand doesn't have that guarantee. Yes `A* ptr = a; if(ptr == nullptr)` will never fail but with `A* ptr = a; ... foo(ptr); ... if(ptr == nullptr)` who knows. The compiler is grabing the low hanging fruit, not doinging full static analysis. – NathanOliver Jun 25 '19 at 15:00
  • Does `A` overload `operator &`? if yes, with `A* ptr = &a;` `ptr` can be `nullptr`. You would need `A* ptr = std::addressof(a)` but this is a function call, and in general function can return `nullptr`. Static analysis is not that simple for those case than checking `this == nullptr`. – Jarod42 Jun 25 '19 at 15:20
  • @BaummitAugen Yea, the standard quotation does answer it, though I find eeroika's answer useful as well. I don't mind marking this as a dupe of the other one. I swear I used search and didn't find it =S. – luk32 Jun 25 '19 at 15:39
  • @NathanOliver I've seen code compiled with gcc 4.9.4 where this passed and was meaningful. Not saying it was good code, but it worked and person who wrote it relied on the check. So there's that. There was vector of pointers to objects of which some got deleted and pointer value zeroed, then a loop over the container called member functions which began with `if(!this) return;`. Something like that. I'm not even sure if this counts as UB, since no one dereferenced invalid pointers until newer compiler removed the check. Unless writing `if(this)` or relying on the result is defined as UB... – luk32 Jun 25 '19 at 15:47
  • @luk32 *I'm not even sure if this counts as UB, since no one dereferenced invalid pointers until newer compiler removed the check. Unless writing if(this) or relying on the result is defined as UB...* You can't call member functions on null pointers. Doing so is UB. The check is useless in well defined code. If you need it for your code to work then the code itself is the problem. – NathanOliver Jun 25 '19 at 15:56
  • @NathanOliver "*You can't call member functions on null pointers.*" I've had problem finding a quote in standard for this. I've read https://stackoverflow.com/a/2474021/1133179 and I tend to agree with the comments. I fail to see clear reasoning that would make it UB. This issue seems unresolved http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#232 am I wrong or was it cleared up? – luk32 Jun 25 '19 at 17:05

1 Answers1

5

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) {
Community
  • 1
  • 1
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Ok, I couldn't make it emit any diagnostic on anything other than `this` but I didn't think of `&a == nullptr`. I guess your answer + standard quotationfrom the answer to the related question solve it. I admit, I thought that verification within same context is easy as with `this`. At least when no-one touches the pointer. I guess it's not, because compiler would need to remember that pointer is non-null for the whole scope. With `this` it doesn't have to. – luk32 Jun 25 '19 at 15:35
  • @luk32 It's technically possible, and even easy to prove the mistake in your example. It's just not possible to write the compiler to detect all cases that are provable, and it takes work to detect even the easy cases. – eerorika Jun 25 '19 at 15:39
  • Sure, I agree. I just thought it's the same as an already implemented diagnostic. This is false. I also knew that both compilers are able to prove it, because they generated assembly using this assumption, but they prove it at optimization stage. It's easier to accept that the diagnostic is not emitted because it would be based on optimization level, or even that it would had to be emitted by optimizer, which might be considered needlessly complicated architecture, etc. – luk32 Jun 25 '19 at 16:00