Why does the enhanced GCC 6 optimizer break practical C++ code?

前端 未结 5 2001
佛祖请我去吃肉
佛祖请我去吃肉 2020-11-28 03:19

GCC 6 has a new optimizer feature: It assumes that this is always not null and optimizes based on that.

Value range propagation now assum

5条回答
  •  天命终不由人
    2020-11-28 03:44

    I guess the question that needs to be answered why well-intentioned people would write the checks in the first place.

    The most common case is probably if you have a class that is part of a naturally occurring recursive call.

    If you had:

    struct Node
    {
        Node* left;
        Node* right;
    };
    

    in C, you might write:

    void traverse_in_order(Node* n) {
        if(!n) return;
        traverse_in_order(n->left);
        process(n);
        traverse_in_order(n->right);
    }
    

    In C++, it's nice to make this a member function:

    void Node::traverse_in_order() {
        // <--- What check should be put here?
        left->traverse_in_order();
        process();
        right->traverse_in_order();
    }
    

    In the early days of C++ (prior to standardization), it was emphasized that that member functions were syntactic sugar for a function where the this parameter is implicit. Code was written in C++, converted to equivalent C and compiled. There were even explicit examples that comparing this to null was meaningful and the original Cfront compiler took advantage of this too. So coming from a C background, the obvious choice for the check is:

    if(this == nullptr) return;      
    

    Note: Bjarne Stroustrup even mentions that the rules for this have changed over the years here

    And this worked on many compilers for many years. When standardization happened, this changed. And more recently, compilers started taking advantage of calling a member function where this being nullptr is undefined behavior, which means that this condition is always false, and the compiler is free to omit it.

    That means that to do any traversal of this tree, you need to either:

    • Do all of the checks before calling traverse_in_order

      void Node::traverse_in_order() {
          if(left) left->traverse_in_order();
          process();
          if(right) right->traverse_in_order();
      }
      

      This means also checking at EVERY call site if you could have a null root.

    • Don't use a member function

      This means that you're writing the old C style code (perhaps as a static method), and calling it with the object explicitly as a parameter. eg. you're back to writing Node::traverse_in_order(node); rather than node->traverse_in_order(); at the call site.

    • I believe the easiest/neatest way to fix this particular example in a way that is standards compliant is to actually use a sentinel node rather than a nullptr.

      // static class, or global variable
      Node sentinel;
      
      void Node::traverse_in_order() {
          if(this == &sentinel) return;
          ...
      }
      

    Neither of the first two options seem that appealing, and while code could get away with it, they wrote bad code with this == nullptr instead of using a proper fix.

    I'm guessing that's how some of these code bases evolved to have this == nullptr checks in them.

提交回复
热议问题