How bad is “if (!this)” in a C++ member function?

前端 未结 11 670
不思量自难忘°
不思量自难忘° 2021-02-04 23:16

If I come across old code that does if (!this) return; in an app, how severe a risk is this? Is it a dangerous ticking time bomb that requires an immediate app-wide

相关标签:
11条回答
  • 2021-02-04 23:46

    It's my personal opinion that you should fail as early as possible to alert you to problems. In that case, I'd unceremoniously remove each and every occurrence of if(!this) I could find.

    0 讨论(0)
  • 2021-02-04 23:47

    In this case I'd suggest removing the NULL check from the member function and create a non-member function

    CThingy* SafeLookup(CLookupThing *lookupThing) {
      if (lookupThing == NULL) {
        return NULL;
      } else {
        return lookupThing->Lookup();
      }
    }
    

    Then it should be easy enough to find every call to the Lookup member function and replace it with the safe non-member function.

    0 讨论(0)
  • 2021-02-04 23:48

    You can safely fix this today by returning a failed lookup object.

    class CLookupThingy: public Interface {
      // ...
    }
    
    class CFailedLookupThingy: public Interface {
     public:
      CThingy* Lookup(string const& name) {
        return NULL;
      }
      operator bool() const { return false; }  // So that GetLookup() can be tested in a condition.
    } failed_lookup;
    
    Interface *GetLookup() {
      if (notReady())
        return &failed_lookup;
      // else etc...
    }
    

    This code still works:

    CThingy *pFoo = GetLookup()->Lookup( "foo" ); // will set pFoo to NULL without crashing
    
    0 讨论(0)
  • 2021-02-04 23:50

    If you have many GetLookup functions return NULL, then you're better off fixing code that calls methods using a NULL pointer. First, replace

    if (!this) return NULL;
    

    with

    if (!this) {
      // TODO(Crashworks): Replace this case with an assertion on July, 2012, once all callers are fixed.
      printf("Please mail the following stack trace to myemailaddress. Thanks!");
      print_stacktrace();
      return NULL;
    }
    

    Now, carry on with your other work, but fix these as they roll in. Replace:

    GetLookup(x)->Lookup(y)...
    

    with

    convert_to_proxy(GetLookup(x))->Lookup(y)...
    

    Where conver_to_proxy does returns the pointer unchanged, unless it's NULL, in which case it returns a FailedLookupObject as in my other answer.

    0 讨论(0)
  • 2021-02-04 23:52

    This is a "ticking bomb" only if you are pedantic about the wording of the specification. However, regardless, it is a terrible, ill-advised approach because it obscures a program error. For that reason alone, I would remove it, even if it means considerable work. It is not an immediate (or even middle-term) risk, but it just isn't a good approach.

    Such error hiding behavior really isn't something you want to rely on, either. Imagine you rely on this behavior (i.e. it doesn't matter whether my objects are valid, it will work anyway!) and then, by some hazard, the compiler optimizes out the if statement in a particular case because it can prove that this is not a null pointer. That is a legitimate optimization not just for some hypothetical future compiler, but for very real, present-time compilers as well.
    But of course, since your program isn't well-formed, it happens that at some point you pass it a null this around 20 corners. Bang, you're dead.
    That's very contrieved, admittedly, and it won't happen, but you cannot be 100% certain that it still cannot possibly happen.

    Note that when I shout out "remove!", that does not mean the whole lot of them must be removed immediately or in one massive manpower operation. You could remove these checks one by one as you encounter them (when you change something in the same file anyway, avoid recompilations), or just text-search for one (preferrably in a highly used function), and remove that one.

    Since you are using GCC, you may be intersted in __builtin_return_address, which may help you remove these checks without massive manpower and totally disrupting the whole workflow and rendering the application entirely unusable.
    Before removing the check, modify it to to output the caller's address, and addr2line will tell you the location in your source. That way, you should be able to quickly identify all the locations in the application that are behaving wrongly, so you can fix these.

    So instead of

    if(!this) return 0;
    

    change one location at a time to something like:

    if(!this) { __builtin_printf("!!! %p\n", __builtin_return_address(0)); return 0; }
    

    That lets you identify the invalid call sites for this change while still letting the program "work as intended" (you can also query the caller's caller if needed). Fix every ill-behaved location, one by one. The program will still "work" as normal.
    Once no more addresses come up, remove the check alltogether. You might still have to fix one or the other crash if you are unlucky (because it didn't show while you tested), but that should be a very rare thing to happen. In any case, it should prevent your co-worker from shouting at you.
    Remove one or two checks per week, and eventually none will be left. Meanwhile, life goes on and nobody notices what you're doing at all.

    TL;DR
    As for "current versions of GCC", you are fine for non-virtual functions, but of course nobody can tell what a future version might do. I deem it however highly unlikely that a future version will cause your code to break. Not few existing projects have this kind of check (I remember we had literally hundreds of them in Code::Blocks code completion at some time, don't ask me why!). Compiler makers probably don't want to make dozens/hundreds of major project maintainers unhappy on purpose, only to prove a point.
    Also, consider the last paragraph ("from a logical point of view"). Even if this check will crash and burn with a future compiler, it will crash and burn anyway.

    The if(!this) return; statement is somewhat useless insofar as this cannot ever be a null pointer in a well-formed program (it means you called a member function on a null pointer). This does not mean, of course, that it couldn't possibly happen. But in this case, the program should crash hard or abort with an assertion. Under no conditions should such a program continue silently.
    On the other hand, it is perfectly possible to call a member function on an invalid object that happens to be not null. Checking whether this is the null pointer obviously doesn't catch that case, but it is the exact same UB. So, apart from hiding wrong behavior, this check also only detects one half of the problematic cases.

    If you are going by the wording of the speficication, using this (which includes checking whether it's a null pointer) is undefined behavior. Insofar, strictly speaking, it is a "time bomb". However, I would not reasonably deem that a problem, both from a practical point of view and from a logical one.

    • From a practical point of view, it doesn't really matter whether you read a pointer that is not valid as long as you do not dereference it. Yes, strictly to the letter, this is not allowed. Yes, in theory someone might build a CPU which will check invalid pointers when you load them, and fault. Alas, this isn't the case, if you're being real.
    • From a logical point of view, assuming that the check will blow up, it still isn't going to happen. For this statement to be executed, the member function must be called, and (virtual or not, inlined or not) is using an invalid this, which it makes available inside the function body. If one illegitimate use of this blows up, the other will, too. Thus, the check is being obsoleted because the program already crashes earlier.


    n.b.: This check is very similar to the "safe delete idiom" which sets a pointer to nullptr after deleting it (using a macro or a templated safe_delete function). Presumably, this is "safe" because it doesn't crash deleting the same pointer twice. Some people even add a redundant if(!ptr) delete ptr;.
    As you know, operator delete is guaranteed to be a no-op on a null pointer. Which means no more and no less than by setting a pointer to the null pointer, you have successfully eliminated the only chance to detect double deletion (which is a program error that needs to be fixed!). It is not any "safer", but it instead hides incorrect program behavior. If you delete an object twice, the program should crash hard.
    You should either leave a deleted pointer alone, or, if you insist on tampering, set it to a non-null invalid pointer (such as the address of a special "invalid" global variable, or a magic value like -1 if you will -- but you should not try to cheat and hide the crash when it is to occur).

    0 讨论(0)
提交回复
热议问题