50

我问自己是否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 顺便说一句。

4

12 回答 12

81

那么 this 指针只是编译时的东西而不是实际的指针吗?

这在很大程度上一个运行时的事情。它指的是调用成员函数的对象,自然该对象可以在运行时存在。

什么编译时的事情是名称查找的工作方式。当编译器遇到x = X它时,它必须弄清楚x分配的这个是什么。所以它查找它,并找到成员变量。由于this->xx引用相同的东西,自然会得到相同的汇编输出。

于 2018-11-12T15:05:58.493 回答
28

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

在非静态 (12.2.1) 成员函数的主体中,关键字this是纯右值表达式,其值是调用该函数的对象的地址。类的this成员函数中的类型XX*

this每次在类自己的代码中引用非静态成员变量或成员函数时,它实际上是隐式的。它也是需要的(无论是隐式还是显式),因为编译器需要在运行时将函数或变量绑定到实际对象。

显式使用它很少有用,除非您需要,例如,在成员函数中消除参数和成员变量之间的歧义。否则,没有它,编译器将使用参数隐藏成员变量(在 Coliru 上实时查看)。

于 2018-11-12T15:20:15.833 回答
17

this当您使用非静态方法时,总是必须存在。无论您是否明确使用它,您都必须引用当前实例,这就是this给您的。

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

于 2018-11-12T15:05:39.057 回答
16

这几乎是x86 中对象如何在程序集级别工作的副本?,在这里我评论了一些示例的 asm 输出,包括显示this指针传入了哪个寄存器。


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

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

在 Godbolt 编译器资源管理器上,使用 System V ABI(RDI 中的第一个参数,RSI 中的第二个参数)为 x86-64 编译,我们得到:

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

我使用 GCC 4.4.3

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

于 2018-11-12T16:03:26.890 回答
10

编译后,每个符号都只是一个地址,所以它不可能是运行时问题。

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

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

  • 在全局命名空间中(如::name),或者在当前命名空间中,或者在使用的命名空间中(当using namespace ...被使用时)
  • 在当前班级
  • 局部定义,在上块
  • 局部定义,在当前块中

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

usingthis->name帮助编译器将搜索范围缩小name到仅在当前类范围内查找,这意味着它会跳过本地定义,如果在类范围内找不到,则不要在全局范围内查找。

于 2018-11-12T15:17:02.217 回答
7

这是一个简单的示例,“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;
}
于 2018-11-12T16:00:19.927 回答
7

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

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

于 2018-11-13T05:49:29.620 回答
5

因为我通常每次引用成员变量或函数时都使用它。

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

让我们回过头来看看它之前是怎么做的,this以了解它是什么this

没有面向对象:

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 中时,它并不会使其“不一个指针”。

于 2018-11-13T12:26:00.357 回答
3

如果编译器内联了一个使用静态而不是动态绑定调用的成员函数,它可能能够优化掉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;
}

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

movl    $10, %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条指令,该指令将第二个函数参数设置为10. 该leaq指令将第一个参数(可能是显式ostream&或隐式this)设置为&cout. 然后程序call对该函数进行a。

但是,在更复杂的情况下(例如,如果您有一个将 anexample&作为参数的函数),编译器需要查找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

您还记得在前面的示例中%rdi,x86-64 上是第一个函数参数,this即调用的隐式指针e.foo(). 将其放在括号中(%rdi),表示在该位置查找变量。(由于example实例中的唯一数据是x&e.x恰好与本例中的相同&e。)将内容移动到%eax设置返回值。

在这种情况下,编译器需要隐式this参数foo(/* example* this */)才能找到&e,因此&e.x. 事实上,在成员函数内部(不是staticxthis->x(*this).x都意味着同样的事情。

于 2018-11-13T05:25:36.803 回答
3

"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;
}

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

于 2018-11-13T15:17:44.097 回答
3

this确实是一个运行时指针(尽管编译器隐式提供了一个),正如大多数答案中所迭代的那样。它用于指示给定成员函数在调用时要操作的类的哪个实例;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),尽管出于语言原因使用不同的格式。事实上,最近 Stroustrup 和 Sutter 都提出了统一 and 的调用语法的x.f(y)提议f(x, y),这将使这种隐含行为成为明确的语言规则。不幸的是,有人担心它可能会给库开发人员带来一些意外的惊喜,因此尚未实施;据我所知,最近的提案是一个联合提案,如果没有找到,f(x,y)就可以依靠x.f(y)f(x,y),类似于之间的交互,例如,std::begin(x)和成员函数x.begin()

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


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

请注意,这些是使用 MSVC 演示的,类布局通过未记录的-d1reportSingleClassLayout 编译器参数输出,因为我发现它比 GCC 或 Clang 等价物更容易阅读。

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

    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()提供的this指针与 不同NonStandardDerived::address(),即使在同一实例上调用也是如此。这是因为后者对 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. 虚拟基类:由于虚拟基类在最派生类之后,this提供给从虚拟基类继承的成员函数的指针将不同于提供给派生类本身的成员的指针。

    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
    

    再一次,基类的成员函数被提供了一个不同的this指针,因为VDerived'inheritedVBase的起始地址与其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指针与传递给不同成员函数的指针不同的情况,即使这两个函数都使用相同的实例调用。这可以出现在除第一个基类之外的任何基类的成员函数中,类似于使用非标准布局类时(其中第一个基类之后的所有基类都从与派生类本身不同的地址开始)......但它在函数的情况下尤其令人惊讶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()返回的值与Derived::raw_address(). 但既然我们知道派生函数总是会调用最派生的形式,那么address()从对 的引用调用时如何正确Base2?这是由于一个叫做“adjustor thunk”的编译器小技巧,它是一个帮助器,它获取基类实例的this指针并在必要时将其调整为指向最派生的类。

    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
    

如果你很好奇,可以随意修改这个小程序,看看如果你多次运行它,地址是如何变化的,或者在它可能具有与你预期不同的值的情况下。

于 2018-11-14T22:53:40.393 回答
2

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();
于 2018-11-14T02:44:59.937 回答