我问自己,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 btw。
答案 0 :(得分:81)
那么this指针只是编译时的东西,而不是实际的指针吗?
很多时候 是运行时的东西。它指的是在其上调用成员函数的对象,该对象自然可以在运行时存在。
什么是 编译时的事情是名称查找的工作方式。当编译器遇到x = X
时,它必须找出正在分配的x
是什么。因此它进行查找,并找到成员变量。由于this->x
和x
指的是同一件事,因此自然会得到相同的程序集输出。
答案 1 :(得分:28)
这是一个实际的指针,如标准所指定(第12.2.2.1节):
每次您在类自己的代码中引用非静态成员变量或成员函数时,在非静态(12.2.1)成员函数的主体中,关键字
this
是prvalue表达式,其值是为其调用该函数的对象的地址。类this
的成员函数中X
的类型为X*
。
this
实际上是隐式的。它也是必需的(无论是隐式还是显式的),因为编译器需要在运行时将函数或变量绑定到实际对象。
显式使用它很少有用,除非您需要例如在成员函数中的参数和成员变量之间消除歧义。否则,没有它,编译器将用参数(See it live on Coliru)遮盖成员变量。
答案 2 :(得分:17)
data
必须始终存在。无论是否显式使用它,都必须引用当前实例,这就是data
所提供的。
在两种情况下,您都将通过this
指针访问内存。只是在某些情况下您可以忽略它。
答案 3 :(得分:16)
这几乎是How do objects work in x86 at the assembly level?的副本,在这里我评论了一些示例的asm输出,包括显示传入了this
指针的寄存器。
在asm中,this
的工作方式与隐藏的第一个arg 完全相同,因此成员函数foo::add(int)
和非成员add
都需要一个 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中的第一个arg,RSI中的第二个arg),我们得到:
# 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
那是released in January 2010,因此它缺少对优化器和错误消息的近十年的改进。 gcc7系列已经停产了一段时间了。期望使用这样的旧编译器错过优化,尤其是对于像AVX这样的现代指令集。
答案 4 :(得分:10)
编译后,每个符号都只是一个地址,因此它不是运行时问题。
即使您没有使用this
,任何成员符号都将被编译为当前类中的偏移量。
在C ++中使用name
时,它可以是下列之一。
::name
),当前名称空间或已使用的名称空间(使用using namespace ...
时)因此,在编写代码时,编译器应从当前块一直到全局名称空间对每个符号进行扫描,以查找符号名称。
使用this->name
有助于编译器缩小对name
的搜索范围,使其仅在当前类范围内查找,这意味着它会跳过局部定义,并且如果在类范围内未找到,则不会查找在全球范围内。
答案 5 :(得分: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;
}
答案 6 :(得分:7)
您的机器对类方法一无所知,它们是幕后的正常功能。
因此,必须始终通过将指针传递给当前对象来实现方法,这在C ++中是隐式的,即T Class::method(...)
只是T Class_Method(Class* this, ...)
的语法糖。
其他语言(如Python或Lua)选择使其显式,而现代的面向对象的C API(如Vulkan(与OpenGL不同))也使用类似的模式。
答案 7 :(得分:5)
因为我通常每次都引用成员变量或函数时才使用它。
在引用成员变量或函数时,您始终使用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时,都不会不要让它“不是指针”。
答案 8 :(得分: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;
}
带有-march=x86-64 -O -S
标志的GCC 7.3.0能够将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
不过,在更复杂的情况下(例如,如果您有一个将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
是第一个函数参数,即对this
的调用的隐式e.foo()
指针。将其放在括号(%rdi)
中意味着在该位置查找变量。 (由于example
实例中的唯一数据是x
,因此在这种情况下&e.x
与&e
相同。)将内容移至%eax
设置返回值。
在这种情况下,编译器需要this
的隐式foo(/* example* this */)
参数才能找到&e
,因此找到&e.x
。实际上,在成员函数(不是static
)中,x
,this->x
和(*this).x
都是同一意思。
答案 9 :(得分: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;
}
(显然,不建议编写此类代码。)
答案 10 :(得分:3)
this
确实是一个运行时指针(尽管编译器隐式地为提供了)。它用于指示给定成员函数在调用时将在哪个类实例上进行操作;对于类c
的任何给定实例C
,当调用任何成员函数cf()
时,将向c.cf()
提供等于{{1}的this
指针}(这自然也适用于类型为&c
的任何结构s
,当调用成员函数S
时,将用于更清晰的演示)。它甚至可以像任何其他指针一样具有cv资格,并且具有相同的效果(但不幸的是,由于特殊,语法也不相同);通常用于s.sf()
正确性,而很少用于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
的特殊变体,尽管出于语言原因其格式不同。实际上,there were recent proposals by both Stroustrup and Sutter统一了f(cv X* this)
和x.f(y)
的调用语法,这将使这种隐式行为成为一个明确的语言规则。不幸的是,它担心可能会给库开发人员带来一些意外的惊喜,因此尚未实施;据我所知,最近的提议是a joint proposal, for f(x,y)
to be able to fall back on x.f(y)
if no f(x,y)
is found,类似于f(x, y)
和成员函数std::begin(x)
之间的交互。
在这种情况下,x.begin()
更类似于普通的指针,程序员可以手动指定它。如果发现一种解决方案可以在不违反最小惊讶原则(或避免其他任何顾虑)的情况下允许采用更健壮的形式,则也可以隐式生成与this
等效的内容作为非成员函数也是如此。
与此相关,需要注意的重要一件事是this
是实例的地址,该实例看到的;尽管指针本身是运行时对象,但并不总是具有您认为的值。当查看具有更复杂的继承层次结构的类时,这变得很重要。具体来说,当查看包含成员函数的一个或多个基类与派生类本身的地址不同时。我特别想到三种情况:
请注意,这些是使用MSVC进行演示的,并且通过undocumented -d1reportSingleClassLayout compiler parameter输出了类布局,这是因为我发现它比GCC或Clang等效项更易于阅读。
非标准布局:当类为标准布局时,实例的第一个数据成员的地址与实例本身的地址完全相同;因此,this
可以说等同于第一个数据成员的地址。即使该数据成员是基类的成员,只要派生类继续遵循标准布局规则,这也将成立。 ...相反,这也意味着,如果派生类不是标准布局,那么将不再保证。
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
不同,即使在同一实例上调用也是如此。这是因为后者对vtable的使用导致编译器插入隐藏成员。
NonStandardDerived::address()
虚拟基类:由于虚拟基位于最衍生类之后,因此提供给从虚拟基继承的成员函数的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
指针与提供给派生类本身的成员的那个。
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
不同的起始地址,基类的成员函数提供了不同的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
指针与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
如果您感到好奇,请随意修改this little program,以查看多次运行地址后地址如何变化,或者在某些情况下地址值可能与您不同期待。
答案 11 :(得分: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();