"this" 指针只是一个编译时的东西吗?

Is the "this" pointer just a compile time thing?

我问自己 this 指针是否会被过度使用,因为我通常每次引用成员变量或函数时都会使用它。我想知道它是否会对性能产生影响,因为必须有一个指针每次都需要取消引用。所以我写了一些测试代码

struct A {
    int x;

    A(int X) {
        x = X; /* And a second time with this->x = X; */
    }
};

int main() {
    A a(8);

    return 0;
}

令人惊讶的是,即使使用 -O0,它们也输出完全相同的汇编代码。

此外,如果我使用一个成员函数并在另一个成员函数中调用它,它会显示相同的行为。那么 this 指针只是编译时的东西而不是实际的指针吗?或者是否存在 this 实际上被翻译和取消引用的情况?我使用 GCC 4.4.3 顺便说一句。

当您使用非静态方法时,

this 始终必须存在。无论您是否显式使用它,都必须引用当前实例,这就是 this 给您的。

在这两种情况下,您都将通过 this 指针访问内存。只是在某些情况下可以省略。

So is the this pointer just a compile time thing and not an actual pointer?

它非常一个运行时间的东西。它指的是调用成员函数的对象,自然那个对象可以存在运行时间。

编译时的事情是名称查找的工作原理。当编译器遇到 x = X 时,它必须弄清楚正在分配的 x 是什么。所以它查找它,并找到成员变量。由于 this->xx 指的是同一事物,自然会得到相同的汇编输出。

编译后,每个符号只是一个地址,所以不会是运行时间问题。

无论如何,任何成员符号都会被编译为当前 class 中的一个偏移量,即使您没有使用 this.

当在C++中使用name时,它可以是以下之一。

  • 在全局命名空间中(如::name),或在当前命名空间中,或在使用的命名空间中(当using namespace ...被使用时)
  • 当前class
  • 本地定义,在上层
  • 本地定义,在当前区块

因此,当您编写代码时,编译器应该以查找符号名称的方式从当前块一直扫描到全局命名空间。

使用 this->name 帮助编译器缩小对 name 的搜索范围,使其仅在当前 class 范围内查找,这意味着它会跳过局部定义,如果在class范围,不要在全局范围内寻找。

它是一个实际的指针,正如标准指定的那样 (§12.2.2.1):

In the body of a non-static (12.2.1) member function, the keyword this is a prvalue expression whose value is the address of the object for which the function is called. The type of this in a member function of a class X is X*.

每次在 class 自己的代码中引用 非静态 成员变量或成员函数时,

this 实际上是隐含的。还需要它(无论是隐式还是显式),因为编译器需要在运行时将函数或变量绑定到实际对象。

显式使用它很少有用,除非您需要,例如,在成员函数中消除参数和成员变量之间的歧义。否则,如果没有它,编译器将使用参数 (See it live on Coliru).

隐藏成员变量

这是一个简单的例子,说明 "this" 在运行时如何发挥作用:

#include <vector>
#include <string>
#include <iostream>

class A;
typedef std::vector<A*> News; 
class A
{
public:
    A(const char* n): name(n){}
    std::string name;
    void subscribe(News& n)
    {
       n.push_back(this);
    }
};

int main()
{
    A a1("Alex"), a2("Bob"), a3("Chris");
    News news;

    a1.subscribe(news);
    a3.subscribe(news);

    std::cout << "Subscriber:";
    for(auto& a: news)
    {
      std::cout << " " << a->name;
    }
    return 0;
}

这几乎是 的副本,我在其中注释了一些示例的 asm 输出,包括显示 this 指针传入的寄存器。


在 asm 中,this 的工作方式与隐藏的第一个 arg 完全相同,因此成员函数 foo::add(int) 和非成员函数 [=15] =] 它采用 explicit foo* 第一个 arg 编译为完全相同的 asm.

struct foo {
    int m;
    void add(int a);  // not inline so we get a stand-alone definition emitted
};

void foo::add(int a) {
    this->m += a;
}

void add(foo *obj, int a) {
    obj->m += a;
}

On the Godbolt compiler explorer,使用 System V ABI 为 x86-64 编译(RDI 中的第一个参数,RSI 中的第二个参数),我们得到:

# gcc8.2 -O3
foo::add(int):
        add     DWORD PTR [rdi], esi   # memory-destination add
        ret
add(foo*, int):
        add     DWORD PTR [rdi], esi
        ret

I use GCC 4.4.3

那是 released in January 2010,因此它缺少近十年来对优化器和错误消息的改进。 gcc7 系列已经发布并稳定了一段时间。预计会错过这样一个旧编译器的优化,尤其是对于像 AVX 这样的现代指令集。

如果编译器内联一个使用静态而非动态绑定调用的成员函数,它可能能够优化掉 this 指针。举个简单的例子:

#include <iostream>

using std::cout;
using std::endl;

class example {
  public:
  int foo() const { return x; }
  int foo(const int i) { return (x = i); }

  private:
  int x;
};

int main(void)
{
  example e;
  e.foo(10);
  cout << e.foo() << endl;
}

带有 -march=x86-64 -O -S 标志的 GCC 7.3.0 能够将 cout << e.foo() 编译为三个指令:

movl    , %esi
leaq    _ZSt4cout(%rip), %rdi
call    _ZNSolsEi@PLT

这是对 std::ostream::operator<< 的调用。请记住 cout << e.foo();std::ostream::operator<< (cout, e.foo()); 的语法糖。 operator<<(int) 可以有两种写法:static operator<< (ostream&, int),作为一个非成员函数,其中左边的操作数是一个显式参数,或 operator<<(int),作为一个成员函数,其中它是隐式 this.

编译器能够推断出 e.foo() 将始终是常量 10。由于 64 位 x86 调用约定是在寄存器中传递函数参数,因此编译为单个 movl 指令,它将第二个函数参数设置为 10leaq 指令将第一个参数(可能是显式 ostream& 或隐式 this)设置为 &cout。然后程序对函数做了一个call

不过,在更复杂的情况下——例如,如果你有一个函数将 example& 作为参数——编译器需要查找 this,因为 this 是什么告诉程序它正在使用哪个实例,因此,要查找哪个实例的 x 数据成员。

考虑这个例子:

class example {
  public:
  int foo() const { return x; }
  int foo(const int i) { return (x = i); }

  private:
  int x;
};

int bar( const example& e )
{
  return e.foo();
}

函数 bar() 被编译成一些样板和指令:

movl    (%rdi), %eax
ret

你还记得前面的例子,x86-64 上的 %rdi 是第一个函数参数,调用 e.foo() 的隐式 this 指针。把它放在括号中,(%rdi),表示在该位置查找变量。 (因为 example 实例中唯一的数据是 x,所以 &e.x 在这种情况下恰好与 &e 相同。)将内容移动到 %eax设置 return 值。

在这种情况下,编译器需要 foo(/* example* this */) 的隐式 this 参数才能找到 &e,从而找到 &e.x。事实上,在成员函数(不是 static)中,xthis->x(*this).x 都是同一个意思。

您的机器对 class 方法一无所知,它们是底层的正常功能。 因此方法必须通过始终传递指向当前对象的指针来实现,它只是隐含在 C++ 中,即 T Class::method(...) 只是 T Class_Method(Class* this, ...).

的语法糖

Python 或 Lua 等其他语言选择显式化,现代面向对象的 C API,如 Vulkan(不同于 OpenGL)使用类似的模式。

since I usually use it every single time I refer to a member variable or function.

当您引用成员变量或函数时,您总是使用this。没有其他方法可以接触到会员。唯一的选择是隐式与显式符号。

我们回过头来看看this之前是怎么做的,了解一下this是什么

没有 OOP:

struct A {
    int x;
};

void foo(A* that) {
    bar(that->x)
}

使用 OOP 但明确地写 this

struct A {
    int x;

    void foo(void) {
        bar(this->x)
    }
};

使用更短的表示法:

struct A {
    int x;

    void foo(void) {
        bar(x)
    }
};

但区别仅在于源代码。所有的都被编译成同样的东西。如果你创建一个成员方法,编译器会为你创建一个指针参数,并将其命名为"this"。如果在引用成员时省略 this->,编译器会很聪明地在大多数情况下为您插入它。而已。唯一的区别是源代码中少了 6 个字母。

当存在歧义时,明确地写 this 是有意义的,即另一个变量的名字就像你的成员变量:

struct A {
    int x;

    A(int x) {
        this->x = x
    }
};

有一些实例,例如 __thiscall,其中 OO 和非 OO 代码在 asm 中的结尾可能略有不同,但是只要指针在堆栈上传递然后优化到寄存器或者在 ECX 中从一开始就没有做到 "not a pointer".

"this" 也可以通过函数参数防止阴影,例如:

class Vector {
   public:
      double x,y,z;
      void SetLocation(double x, double y, double z);
};

void Vector::SetLocation(double x, double y, double z) {
   this->x = x; //Passed parameter assigned to member variable
   this->y = y;
   this->z = z;
}

(显然,不鼓励编写这样的代码。)

this 是一个指针。它就像是每个方法的一部分的隐式参数。您可以想象使用纯 C 函数并编写如下代码:

Socket makeSocket(int port) { ... }
void send(Socket *this, Value v) { ... }
Value receive(Socket *this) { ... }

Socket *mySocket = makeSocket(1234);
send(mySocket, someValue); // The subject, `mySocket`, is passed in as a param called "this", explicitly
Value newData = receive(socket);

在 C++ 中,类似的代码可能如下所示:

mySocket.send(someValue); // The subject, `mySocket`, is passed in as a param called "this"
Value newData = mySocket.receive();

this 确实是一个 运行 时间指针(尽管编译器隐式 提供 ),正如大多数答案中所重复的那样。它用于指示给定成员函数在调用时将对 class 的哪个实例进行操作;对于 class C 的任何给定实例 c,当任何成员函数 cf() 被调用时,c.cf() 将提供一个 this 指针等于&c(这自然也适用于类型 S 的任何结构 s,当调用成员函数 s.sf() 时,将用于更清晰的演示)。它甚至可以像任何其他指针一样被 cv 限定,具有相同的效果(但不幸的是,由于特殊,语法不同);这通常用于 const 正确性,而很少用于 volatile 正确性。

template<typename T>
uintptr_t addr_out(T* ptr) { return reinterpret_cast<uintptr_t>(ptr); }

struct S {
    int i;

    uintptr_t address() const { return addr_out(this); }
};

// Format a given numerical value into a hex value for easy display.
// Implementation omitted for brevity.
template<typename T>
std::string hex_out_s(T val, bool disp0X = true);

// ...

S s[2];

std::cout << "Control example: Two distinct instances of simple class.\n";
std::cout << "s[0] address:\t\t\t\t"        << hex_out_s(addr_out(&s[0]))
          << "\n* s[0] this pointer:\t\t\t" << hex_out_s(s[0].address())
          << "\n\n";
std::cout << "s[1] address:\t\t\t\t"        << hex_out_s(addr_out(&s[1]))
          << "\n* s[1] this pointer:\t\t\t" << hex_out_s(s[1].address())
          << "\n\n";

示例输出:

Control example: Two distinct instances of simple class.
s[0] address:                           0x0000003836e8fb40
* s[0] this pointer:                    0x0000003836e8fb40

s[1] address:                           0x0000003836e8fb44
* s[1] this pointer:                    0x0000003836e8fb44

无法保证这些值,并且可以很容易地从一次执行更改为下一次执行;通过使用构建工具,在创建和测试程序时最容易观察到这一点。


从机制上讲,它类似于添加到每个成员函数参数列表开头的隐藏参数; x.f() cv 可以看作是 f(cv X* this) 的特殊变体,尽管由于语言原因具有不同的格式。实际上,there were recent proposals by both Stroustrup and Sutter to unify the call syntax of x.f(y) and f(x, y), which would've made this implicit behaviour an explicit linguistic rule. It unfortunately was met with concerns that it may cause a few unwanted surprises for library developers, and thus not yet implemented; to my knowledge, the most recent proposal is a joint proposal, for f(x,y) to be able to fall back on x.f(y) if no f(x,y) is found,类似于std::begin(x)和成员函数x.begin().

之间的交互

在这种情况下,this 更类似于普通指针,程序员可以手动指定它。如果找到一个解决方案允许更健壮的形式而不违反最小惊讶原则(或带来任何其他问题),那么也可以隐式生成 this 的等价物作为普通指针非成员函数,以及。


与此相关,需要注意的一件重要事情是 this 是实例的地址,如该实例所见;虽然指针本身是 运行 时间的东西,但它并不总是具有您认为的价值。当查看具有更复杂继承层次结构的 classes 时,这变得很重要。具体来说,在查看包含成员函数的一个或多个基 class 与派生 class 本身的地址不同的情况时。特别想到三个案例:

请注意,这些是使用 MSVC 演示的,通过 undocumented -d1reportSingleClassLayout compiler parameter 输出 class 布局,因为我发现它比 GCC 或 Clang 等价物更容易阅读。

  1. 非标准布局:当class为标准布局时,实例的第一个数据成员的地址与地址完全相同实例本身;因此,this 可以说等同于第一个数据成员的地址。即使所述数据成员是基础 class 的成员,只要派生 class 继续遵循标准布局规则,这也适用。 ...相反,这也意味着如果派生的 class 不是 标准布局,则不再保证。

    struct StandardBase {
        int i;
    
        uintptr_t address() const { return addr_out(this); }
    };
    
    struct NonStandardDerived : StandardBase {
        virtual void f() {}
    
        uintptr_t address() const { return addr_out(this); }
    };
    
    static_assert(std::is_standard_layout<StandardBase>::value, "Nyeh.");
    static_assert(!std::is_standard_layout<NonStandardDerived>::value, ".heyN");
    
    // ...
    
    NonStandardDerived n;
    
    std::cout << "Derived class with non-standard layout:"
              << "\n* n address:\t\t\t\t\t"                      << hex_out_s(addr_out(&n))
              << "\n* n this pointer:\t\t\t\t"                   << hex_out_s(n.address())
              << "\n* n this pointer (as StandardBase):\t\t"     << hex_out_s(n.StandardBase::address())
              << "\n* n this pointer (as NonStandardDerived):\t" << hex_out_s(n.NonStandardDerived::address())
              << "\n\n";
    

    示例输出:

    Derived class with non-standard layout:
    * n address:                                    0x00000061e86cf3c0
    * n this pointer:                               0x00000061e86cf3c0
    * n this pointer (as StandardBase):             0x00000061e86cf3c8
    * n this pointer (as NonStandardDerived):       0x00000061e86cf3c0
    

    请注意 StandardBase::address() 提供了与 NonStandardDerived::address() 不同的 this 指针,即使在同一实例上调用也是如此。这是因为后者使用了vtable导致编译器插入了隐藏成员

    class StandardBase      size(4):
            +---
     0      | i
            +---
    class NonStandardDerived        size(16):
            +---
     0      | {vfptr}
            | +--- (base class StandardBase)
     8      | | i
            | +---
            | <alignment member> (size=4)
            +---
    NonStandardDerived::$vftable@:
            | &NonStandardDerived_meta
            |  0
     0      | &NonStandardDerived::f 
    NonStandardDerived::f this adjustor: 0
    
  2. 虚碱基classes:由于虚碱基尾随最衍生的class,this 提供给从虚拟基类继承的成员函数的指针将不同于提供给派生 class 本身成员的指针。

    struct VBase {
        uintptr_t address() const { return addr_out(this); }
    };
    struct VDerived : virtual VBase {
        uintptr_t address() const { return addr_out(this); }
    };
    
    // ...
    
    VDerived v;
    
    std::cout << "Derived class with virtual base:"
              << "\n* v address:\t\t\t\t\t"              << hex_out_s(addr_out(&v))
              << "\n* v this pointer:\t\t\t\t"           << hex_out_s(v.address())
              << "\n* this pointer (as VBase):\t\t\t"    << hex_out_s(v.VBase::address())
              << "\n* this pointer (as VDerived):\t\t\t" << hex_out_s(v.VDerived::address())
              << "\n\n";
    

    示例输出:

    Derived class with virtual base:
    * v address:                                    0x0000008f8314f8b0
    * v this pointer:                               0x0000008f8314f8b0
    * this pointer (as VBase):                      0x0000008f8314f8b8
    * this pointer (as VDerived):                   0x0000008f8314f8b0
    

    基 class' 成员函数再次提供了不同的 this 指针,因为 VDerived 继承的 VBase 具有不同的起始地址比 VDerived 本身。

    class VDerived  size(8):
            +---
     0      | {vbptr}
            +---
            +--- (virtual base VBase)
            +---
    VDerived::$vbtable@:
     0      | 0
     1      | 8 (VDerivedd(VDerived+0)VBase)
    vbi:       class  offset o.vbptr  o.vbte fVtorDisp
               VBase       8       0       4 0
    
  3. 多重继承: 正如所料,多重继承很容易导致传递给一个成员函数的this指针不同的情况比 this 指针传递给不同的成员函数,即使这两个函数是用同一个实例调用的。这可以用于除第一个以外的任何基 class 的成员函数,类似于使用非标准布局 classes 时(其中第一个之后的所有基 classes 开始于与派生的 class 本身不同的地址)...但是在 virtual 函数的情况下,当多个成员提供具有相同签名的虚函数时,这尤其令人惊讶。

    struct Base1 {
        int i;
    
        virtual uintptr_t address() const { return addr_out(this); }
        uintptr_t raw_address() { return addr_out(this); }
    };
    struct Base2 {
        short s;
    
        virtual uintptr_t address() const { return addr_out(this); }
        uintptr_t raw_address() { return addr_out(this); }
    };
    struct Derived : Base1, Base2 {
        bool b;
    
        uintptr_t address() const override { return addr_out(this); }
        uintptr_t raw_address() { return addr_out(this); }
    };
    
    // ...
    
    Derived d;
    
    std::cout << "Derived class with multiple inheritance:"
              << "\n  (Calling address() through a static_cast reference, then the appropriate raw_address().)"
              << "\n* d address:\t\t\t\t\t"               << hex_out_s(addr_out(&d))
              << "\n* d this pointer:\t\t\t\t"            << hex_out_s(d.address())                          << " (" << hex_out_s(d.raw_address())          << ")"
              << "\n* d this pointer (as Base1):\t\t\t"   << hex_out_s(static_cast<Base1&>((d)).address())   << " (" << hex_out_s(d.Base1::raw_address())   << ")"
              << "\n* d this pointer (as Base2):\t\t\t"   << hex_out_s(static_cast<Base2&>((d)).address())   << " (" << hex_out_s(d.Base2::raw_address())   << ")"
              << "\n* d this pointer (as Derived):\t\t\t" << hex_out_s(static_cast<Derived&>((d)).address()) << " (" << hex_out_s(d.Derived::raw_address()) << ")"
              << "\n\n";
    

    示例输出:

    Derived class with multiple inheritance:
      (Calling address() through a static_cast reference, then the appropriate raw_address().)
    * d address:                                    0x00000056911ef530
    * d this pointer:                               0x00000056911ef530 (0x00000056911ef530)
    * d this pointer (as Base1):                    0x00000056911ef530 (0x00000056911ef530)
    * d this pointer (as Base2):                    0x00000056911ef530 (0x00000056911ef540)
    * d this pointer (as Derived):                  0x00000056911ef530 (0x00000056911ef530)
    

    我们希望每个 raw_address() 遵循相同的规则,因为每个明确地是一个单独的函数,因此 Base2::raw_address() 将 return 与 Derived::raw_address() 不同的值。但是因为我们知道派生函数总是调用最派生的形式,所以当从对 Base2 的引用调用时 address() 是如何正确的?这是由于一个叫做 "adjustor thunk" 的小编译器技巧造成的,它是一个助手,它接受一个基础 class 实例的 this 指针并将其调整为指向最派生的 class 相反,在必要时。

    class Derived   size(40):
            +---
            | +--- (base class Base1)
     0      | | {vfptr}
     8      | | i
            | | <alignment member> (size=4)
            | +---
            | +--- (base class Base2)
    16      | | {vfptr}
    24      | | s
            | | <alignment member> (size=6)
            | +---
    32      | b
            | <alignment member> (size=7)
            +---
    Derived::$vftable@Base1@:
            | &Derived_meta
            |  0
     0      | &Derived::address 
    Derived::$vftable@Base2@:
            | -16
     0      | &thunk: this-=16; goto Derived::address 
    Derived::address this adjustor: 0
    

如果你很好奇,请随意修改 this little program,看看如果你多次 运行 地址会发生什么变化,或者在它可能有的情况下与您预期不同的值。