qdii
qdii

Reputation: 12983

When is the value of "this" shifted by an offset?

I was wondering whether assert( this != nullptr ); was a good idea in member functions and someone pointed out that it wouldn’t work if the value of this had been added an offset. In that case, instead of being 0, it would be something like 40, making the assert useless.

When does this happen though?

Upvotes: 0

Views: 262

Answers (6)

Itay Maman
Itay Maman

Reputation: 30733

this adjustment can happen only in classes that use multiple-inheritance. Here's a program that illustrates 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;
}

When I run this program I get this output:

this=0
this=0x4

That is: your assert this != nullptr will not catch the invocation of c->bf() where c is nullptr because the this of the B sub-object inside the C object is shifted by four bytes (due to the A sub-object).

Let's try to illustrate the layout of a C object:

0:  | n |
4:  | m |

the numbers on the left-hand-side are offsets from the object's beginning. So, at offset 0 we have the A sub-object (with its data member n). at offset 4 we have the B sub-objects (with its data member m).

The this of the entire object, as well as the this of the A sub-object both point at offset 0. However, when we want to refer to the B sub-object (when invoking a method defined by B) the this value need to be adjusted such that it points at the beginning of the B sub-object. Hence the +4.

Upvotes: 4

Casey
Casey

Reputation: 42594

Compilers typically implement multiple inheritance by storing the base objects sequentially in memory. If you had, e.g.:

struct bar {
  int x;
  int something();
};

struct baz {
  int y;
  int some_other_thing();
};

struct foo : public bar, public baz {};

The compiler will allocate foo and bar at the same address, and baz will be offset by sizeof(bar). So, under some implementation, it's possible that nullptr -> some_other_thing() results in a non-null this.

This example at Coliru demonstrates (assuming the result you get from the undefined behavior is the same one I did) the situation, and shows an assert(this != nullptr) failing to detect the case. (Credit to @DyP who I basically stole the example code from).

Upvotes: 0

dyp
dyp

Reputation: 39151

Note this is UB anyway.

Multiple inheritance can introduce an offset, depending on the implementation:

#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();
}

Output on some version of clang++:

0
0x4

Live example.


Also note, as I pointed out in the comments to the OP, that this assert might not work for virtual function calls, as the vtable isn't initialized (if the compiler does a dynamic dispatch, i.e. doesn't optimize if it know the dynamic type of *p).

Upvotes: 1

Anand Rathi
Anand Rathi

Reputation: 786

I think its not that bad a idea to put assert, for example atleast it can catch see below example

class Test{
public:
void DoSomething() {
  std::cout << "Hello";
}
};

int main(int argc , char argv[]) {
Test* nullptr = 0;
 nullptr->DoSomething();
}

The above example will run without error, If more complex becomes difficult to debug if that assert is absent.

I am trying to make a point that null this pointer can go unnoticed, and in complex situation becomes difficult to debug , I have faced this situation.

Upvotes: -3

Hans Passant
Hans Passant

Reputation: 942408

Multiple inheritance can cause an offset, skipping the extra v-table pointers in the object. The generic name is "this pointer adjustor thunking".

But you are helping too much. Null references are very common bugs, the operating system already has an assert built-in for you. Your program will stop with a segfault or access violation. The diagnostic you'll get from the debugger is always good enough to tell you that the object pointer is null, you'll see a very low address. Not just null, it works for MI cases as well.

Upvotes: 8

Vaughn Cato
Vaughn Cato

Reputation: 64308

Here is a situation where it might happen:

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!
}

This is undefined behavior for C++ in general, but with some compilers, it might have the consistent behavior of the this pointer having some small non-zero address inside A::f().

Upvotes: 0

Related Questions