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)
, хотя и с другим форматом по лингвистическим причинам.Фактически, были недавние предложения как Страуструпа, так и Саттера об унификации синтаксиса вызовов x.f(y)
и f(x, y)
, что сделало бы это неявное поведение явным лингвистическим правилом.К сожалению, он был встречен с опасениями, что это может вызвать несколько нежелательных сюрпризов для разработчиков библиотек, и, следовательно, еще не реализовано;Насколько мне известно, самое последнее предложение является совместным предложением, для f(x,y)
можно использовать x.f(y)
, если f(x,y)
не найдено , аналогично взаимодействию, например, std::begin(x)
и функция-член x.begin()
.
В этом случае this
будет больше похож на обычный указатель, и программист сможет указать его вручную.Если будет найдено решение, позволяющее создать более надежную форму без нарушения принципа наименьшего удивления (или принятия каких-либо других проблем), то эквивалентный this
также может быть неявно сгенерирован как обычный указатель дляКроме того, функции-члены.
Также важно отметить, что this
- это адрес экземпляра, , как видно из этого экземпляра ;в то время как сам указатель является элементом времени выполнения, он не всегда имеет значение, которое, как вы думаете, имеет.Это становится актуальным при рассмотрении классов с более сложной иерархией наследования.В частности, при рассмотрении случаев, когда один или несколько базовых классов, которые содержат функции-члены, не имеют тот же адрес, что и сам производный класс.В частности, на ум приходят три случая:
Обратите внимание, что они демонстрируются с использованием MSVC, с разметкой классов, выводимой с помощью параметра компилятора undocumented -d1reportSingleClassLayout , потому что мне было легче его найтичитаемые, чем эквиваленты GCC или Clang.
Нестандартная компоновка: Когда класс является стандартной компоновкой, адрес первого члена данных экземпляра точноидентичен адресу самого экземпляра;таким образом, можно сказать, что 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
Виртуальные базовые классы: Из-за виртуальных баз, тянущихся после самого производного класса, указатель 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
?Это происходит из-за небольшого трюка с компилятором, называемого «корректором 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
Если вам интересно, не стесняйтесь возиться с этой маленькой программой , чтобы посмотреть, как меняются адреса, если вы запускаете ее несколько раз илислучаи, когда оно может иметь значение, отличное от ожидаемого.