私は通常、メンバー変数または関数を参照するたびに使用するため、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ポインターはコンパイル時のものであり、実際のポインターではありませんか?
非常にis実行時のものです。メンバー関数が呼び出されるオブジェクトを参照します。当然、オブジェクトは実行時に存在できます。
isコンパイル時のことは、名前検索の仕組みです。コンパイラがx = X
に遭遇すると、このx
が割り当てられているものを把握する必要があります。そのため、それを検索し、メンバー変数を見つけます。 this->x
とx
は同じものを参照するため、当然、同じAssembly出力を取得します。
標準で指定されているように、これは実際のポインタです(§12.2.2.1):
非静的(12.2.1)メンバー関数の本文では、キーワード
this
は値が関数が呼び出されるオブジェクトのアドレスであるprvalue式です。クラスthis
のメンバー関数のX
の型はX*
です。
this
は、クラス独自のコード内でnon-staticメンバー変数またはメンバー関数を参照するたびに実際に暗黙的です。また、コンパイラーは、実行時に関数または変数を実際のオブジェクトに結び付ける必要があるため、(暗黙的または明示的に)必要です。
たとえば、メンバー関数内でパラメーターとメンバー変数を明確に区別する必要がない限り、明示的に使用することはめったに役立ちません。それ以外の場合、それなしでは、コンパイラーはパラメーターでメンバー変数をシャドーします( Coliruでライブを参照 )。
this
は、非静的メソッドを使用している場合は常に存在する必要があります。明示的に使用するかどうかにかかわらず、現在のインスタンスへの参照が必要です。これがthis
が提供するものです。
どちらの場合も、this
ポインターを介してメモリにアクセスします。場合によっては省略できます。
これは、 アセンブリレベルでx86でオブジェクトがどのように機能するか とほぼ同じです。ここで、this
ポインターが渡されたレジスターの表示など、いくつかの例のasm出力についてコメントします。
asmでは、this
は隠された最初のargとまったく同じように機能するため、メンバー関数foo::add(int)
と非メンバーadd
の両方がexplicitfoo*
最初の引数は、まったく同じ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;
}
Godboltコンパイラエクスプローラー 、System V ABI(RDIの最初の引数、RSIの2番目の引数)で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月にリリースされた であったため、オプティマイザーとエラーメッセージの10年近くの改善が欠けています。 gcc7シリーズはしばらくの間、安定して使用されています。特にAVXのような最新の命令セットでは、このような古いコンパイラで見逃された最適化を期待してください。
コンパイル後、すべてのシンボルは単なるアドレスであるため、実行時の問題になることはありません。
this
を使用しなかった場合でも、メンバーシンボルは現在のクラスのオフセットにコンパイルされます。
name
がC++で使用される場合、次のいずれかになります。
::name
など)、現在の名前空間、または使用されている名前空間(using namespace ...
が使用されている場合)したがって、コードを記述するとき、コンパイラは、シンボル名を検索する方法で、現在のブロックからグローバル名前空間までそれぞれをスキャンする必要があります。
this->name
を使用すると、コンパイラはname
の検索を絞り込んで現在のクラススコープでのみ検索します。つまり、ローカル定義をスキップし、クラススコープで見つからない場合は検索しません。グローバルスコープ。
以下は、実行時に「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;
}
あなたのマシンはクラスメソッドについて何も知りません、それらは内部の通常の機能です。したがって、常に現在のオブジェクトにポインターを渡すことでメソッドを実装する必要があります。これは、C++では暗黙的です。つまり、T Class::method(...)
はT Class_Method(Class* this, ...)
の単なる構文糖です。
PythonやLuaのような他の言語はそれを明示的にすることを選択し、Vulkanのような最新のオブジェクト指向C APIは(OpenGLとは異なり)同様のパターンを使用します。
通常、メンバー変数または関数を参照するたびに使用するためです。
alwaysメンバー変数または関数を参照するときは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で登録または登録しても、「ポインタではありません」。
「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
ポインターを最適化することができます。次の簡単な例をご覧ください。
#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()
を3つの命令にコンパイルできます。
movl $10, %esi
leaq _ZSt4cout(%rip), %rdi
call _ZNSolsEi@PLT
これはstd::ostream::operator<<
の呼び出しです。 cout << e.foo();
はstd::ostream::operator<< (cout, e.foo());
の構文糖衣であることを忘れないでください。 operator<<(int)
は、2つの方法で記述できます。static operator<< (ostream&, int)
、左のオペランドが明示的なパラメーターである非メンバー関数として、またはoperator<<(int)
、メンバー関数として、暗黙的にthis
。
コンパイラは、e.foo()
が常に定数10
であると推定できました。 64ビットのx86呼び出し規約はレジスタに関数の引数を渡すことであるため、2番目の関数パラメーターを10
に設定する単一のmovl
命令にコンパイルされます。 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
が最初の関数引数であり、e.foo()
への呼び出しに対する暗黙のthis
ポインターであることを覚えています。 (%rdi)
という括弧で囲むと、その場所で変数を検索することを意味します。 (example
インスタンスのデータはx
のみであるため、この場合、&e.x
は&e
と同じになります。)内容を%eax
に移動する戻り値を設定します。
この場合、コンパイラーは、&e
、したがって&e.x
を見つけるために、foo(/* example* this */)
への暗黙的なthis
引数が必要でした。実際、メンバー関数(static
ではない)、x
、this->x
、および(*this).x
はすべて同じことを意味します。
this
は、ほとんどの回答で反復されているように、実際にはランタイムポインタです(暗黙的にsuppliedですが)。これは、指定されたメンバー関数が呼び出されたときに操作するクラスのインスタンスを示すために使用されます。クラスc
の任意のインスタンスC
について、メンバー関数cf()
が呼び出されると、c.cf()
に&c
に等しいthis
ポインターが提供されます(これは自然にクリーナーデモンストレーションに使用されるように、メンバー関数s.sf()
を呼び出すときに、タイプs
のstruct S
にも適用されます。他のポインターと同じように、同じ効果で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の両方による最近の提案がありました は、x.f(y)
とf(x, y)
の呼び出し構文を統一し、この暗黙の動作を明示的な言語規則にしました。残念なことに、ライブラリ開発者にいくつかの望ましくない驚きを引き起こす可能性があるという懸念があり、まだ実装されていません。私の知る限り、最新の提案は f(x,y)
が見つからない場合にx.f(y)
にフォールバックできるようにするためのf(x,y)
の共同提案 です。たとえば、std::begin(x)
とメンバー関数x.begin()
との間の相互作用。
この場合、this
は通常のポインターに似ており、プログラマーは手動で指定できます。最小の驚きの原則に違反することなく(または他の懸念を解消することなく)、より堅牢な形式を可能にするソリューションが見つかった場合、this
と同等の値も、通常のポインタとして暗黙的に生成できます。非メンバー関数も同様です。
関連して、注意すべき重要なことの1つは、this
がインスタンスのアドレスであるそのインスタンスで表示される;です。ポインター自体は実行時のものですが、常にあなたが持っていると思う値を持っているとは限りません。これは、より複雑な継承階層を持つクラスを見るときに重要になります。具体的には、メンバー関数を含む1つまたは複数の基本クラスが、派生クラス自体と同じアドレスを持たない場合を調べます。特に3つのケースが思い浮かびます:
これらは、MSVCを使用してデモンストレーションされることに注意してください。クラスレイアウトは ndocumented -d1reportSingleClassLayoutコンパイラパラメーター を介して出力されます。これは、GCCまたはClangの同等のものより読みやすいためです。
非標準レイアウト:クラスが標準レイアウトの場合、インスタンスの最初のデータメンバーのアドレスはインスタンス自体のアドレスとまったく同じです。したがって、this
は、最初のデータメンバーのアドレスと同等であると言えます。これは、派生したクラスが標準のレイアウトルールに従っている限り、そのデータメンバーが基本クラスのメンバーである場合にも当てはまります。 ...逆に、これは、派生クラスis n'tが標準レイアウトの場合、これが保証されなくなることも意味します。
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
仮想ベースクラス:最も派生したクラスの後に続く仮想ベースのため、仮想から継承されたメンバー関数に提供されるthis
ポインターbaseは、派生クラス自体のメンバーに提供されるものとは異なります。
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
複数の継承:予想されるように、複数の継承は、1つのメンバー関数に渡される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()
とは異なる値を返します。しかし、派生関数は常に最も派生したフォームを呼び出すことがわかっているので、Base2
への参照から呼び出された場合、address()
はどのように正しいのでしょうか?これは、「アジャスターサンク」と呼ばれる小さなコンパイラートリックが原因です。これは、基本クラスインスタンスの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
はポインターです。これは、すべてのメソッドの一部である暗黙的なパラメーターのようなものです。プレーンな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();