【问题标题】:When is the value of "this" shifted by an offset?“this”的值何时偏移偏移量?
【发布时间】:2013-07-21 13:21:19
【问题描述】:

我想知道 assert( this != nullptr ); 在成员函数中是否是一个好主意,有人指出如果 this 的值添加了偏移量,它就行不通了。在这种情况下,它不是 0,而是 40,这使得断言毫无用处。

什么时候会发生这种情况?

【问题讨论】:

  • 在空指针上调用函数不是未定义的行为吗?
  • 如果 this 是通过索引对象数组而派生的,其中数组基础指针为空。
  • @jogojapan well assert()其实是用来调试bug的:)
  • @qdii 我明白你在说什么。但是 undefined behavior 意味着当您在代码中进行这样的调用时,没有定义会发生什么。特别是,不能保证永远到达assert 的点。相关:stackoverflow.com/questions/2474018/…
  • 优化编译器倾向于在不发生未定义行为的假设下进行优化。取消引用空指针显然是未定义的行为,因此 assert(this != NULL); 可以完全优化。

标签: c++ pointers this


【解决方案1】:

多重继承会导致偏移,跳过对象中额外的 v-table 指针。通用名称是“this pointer adjustor thunking”。

但是你帮的太多了。空引用是非常常见的错误,操作系统已经为您内置了一个断言。您的程序将因段错误或访问冲突而停止。您将从调试器获得的诊断结果总是足以告诉您对象指针为空,您会看到一个非常低的地址。不只是 null,它也适用于 MI 案例。

【讨论】:

  • +1 好点,感谢“这个指针调整器thunking”链接。
  • 我觉得这个答案有些误导。 C++ 并非建立在它必须在支持内存保护(或分段)的系统上运行的假设之上。此外,即使确实如此,分段错误通常也不会提供非常好的信息。对于堆栈跟踪,您需要一个核心转储,这取决于崩溃的情况。为了使堆栈跟踪有用,您需要调试符号。最后,在空指针上调用成员函数是未定义的行为。 UB 与分段错误不同,即使在具有内存分段的系统上也是如此。
【解决方案2】:

this 调整只能发生在使用多重继承的类中。这是一个说明这一点的程序:

#include <iostream>

using namespace std;

struct A {
  int n;
  void af() { cout << "this=" << this << endl; }
};

struct B {
  int m;
  void bf() { cout << "this=" << this << endl; }
};

struct C : A,B {
};

int main(int argc, char** argv) {

  C* c = NULL;
  c->af();
  c->bf();

  return 0;
}

当我运行这个程序时,我得到这个输出:

this=0
this=0x4

也就是说:您的 assert this != nullptr 不会捕获 c-&gt;bf() 的调用,其中 c 是 nullptr,因为 C 对象内的 B 子对象的 this 移动了四个字节(由于A 子对象)。

让我们尝试说明C对象的布局:

0:  | n |
4:  | m |

左侧的数字是从对象开始的偏移量。因此,在偏移量0 处,我们有A 子对象(及其数据成员n)。在偏移量4 我们有B 子对象(及其数据成员m)。

整个对象的this,以及A子对象的this都指向偏移量0。但是,当我们要引用B子对象时(当调用由B 定义的方法)需要调整this 值,使其指向B 子对象的开头。因此 +4。

【讨论】:

    【解决方案3】:

    请注意这是 UB。

    多重继承可以引入偏移量,具体取决于实现:

    #include <iostream>
    
    struct wup
    {
        int i;
        void foo()
        {
            std::cout << (void*)this << std::endl;
        }
    };
    
    struct dup
    {
        int j;
        void bar()
        {
            std::cout << (void*)this << std::endl;
        }
    };
    
    struct s : wup, dup
    {
        void foobar()
        {
            foo();
            bar();
        }
    };
    
    int main()
    {
        s* p = nullptr;
        p->foobar();
    }
    

    在某些版本的 clang++ 上的输出:

    0
    0x4

    Live example.


    还请注意,正如我在 OP 的 cmets 中指出的那样,此 assert 可能不适用于虚函数调用,因为 vtable 未初始化(如果编译器执行动态调度,即不如果知道*p的动态类型,则进行优化。

    【讨论】:

    • 我很好奇 - clang 优化了assert(this != NULL)
    • @Casey 在哪个优化级别上?
    • @Casey -O0,否(不是链接的 clang++ 版本)。
    • 对我来说最大就够了。我知道 clang 在其他未定义行为的情况下会做一些有趣的事情。 (我真的需要在家里的 Windows 机器上设置 clang)。
    • 其实看了链接,没想到Coliru有clang。
    【解决方案4】:

    这是可能发生的情况:

    struct A {
        void f()
        {
           // this assert will probably not fail
           assert(this!=nullptr);
        }
    };
    
    struct B {
        A a1;
        A a2;
    };
    
    static void g(B *bp)
    {
        bp->a2.f(); // undefined behavior at this point, but many compilers will
                    // treat bp as a pointer to address zero and add sizeof(A) to
                    // the address and pass it as the this pointer to A::f().
    
    }
    
    int main(int,char**)
    {
        g(nullptr); // oops passed null!
    }
    

    这对于 C++ 通常是未定义的行为,但对于某些编译器,它可能具有 this 指针的一致行为在 A::f() 内部具有一些小的非零地址。

    【讨论】:

    • 通常情况下,我会将assert 放在g 中,所以f 中的assert 只是另一层调试?
    【解决方案5】:

    编译器通常通过将基础对象顺序存储在内存中来实现多重继承。如果你有,例如:

    struct bar {
      int x;
      int something();
    };
    
    struct baz {
      int y;
      int some_other_thing();
    };
    
    struct foo : public bar, public baz {};
    

    编译器会将foobar分配到同一个地址,baz会偏移sizeof(bar)。因此,在某些实现下,nullptr -&gt; some_other_thing() 可能会导致非空 this

    This example at Coliru 演示(假设您从未定义行为中获得的结果与我所做的相同)情况,并显示assert(this != nullptr) 未能检测到该情况。 (感谢@DyP,我基本上是从他那里窃取了示例代码)。

    【讨论】:

      【解决方案6】:

      我认为放置断言是一个不错的主意,例如至少它可以捕捉到下面的例子

      class Test{
      public:
      void DoSomething() {
        std::cout << "Hello";
      }
      };
      
      int main(int argc , char argv[]) {
      Test* nullptr = 0;
       nullptr->DoSomething();
      }
      

      上面的例子将运行没有错误,如果没有断言,如果更复杂变得难以调试。

      我试图指出 null 这个指针可能会被忽视,并且在复杂的情况下变得难以调试,我遇到了这种情况。

      【讨论】:

      • 我不明白您如何断言无条件调用未定义行为的程序“将无错误地运行。”
      • 我认为多重继承会为(某些)基类中的函数引入偏移量。
      • 问题是什么时候会有偏移量,所以这没有回答。
      • 在判断之前尝试@Casey
      • @interjayi 我试图回答这个“我想知道 assert( this != nullptr ); 在成员函数中是否是个好主意”
      猜你喜欢
      • 1970-01-01
      • 2011-08-04
      • 1970-01-01
      • 1970-01-01
      • 2011-09-02
      • 2014-02-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多