web-dev-qa-db-ja.com

「this」ポインターは単なるコンパイル時のものですか?

私は通常、メンバー変数または関数を参照するたびに使用するため、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を使用します。

47
Yastanub

Thisポインターはコンパイル時のものであり、実際のポインターではありませんか?

非常にis実行時のものです。メンバー関数が呼び出されるオブジェクトを参照します。当然、オブジェクトは実行時に存在できます。

isコンパイル時のことは、名前検索の仕組みです。コンパイラがx = Xに遭遇すると、このxが割り当てられているものを把握する必要があります。そのため、それを検索し、メンバー変数を見つけます。 this->xxは同じものを参照するため、当然、同じAssembly出力を取得します。

81
StoryTeller

標準で指定されているように、これは実際のポインタです(§12.2.2.1):

非静的(12.2.1)メンバー関数の本文では、キーワードthisは値が関数が呼び出されるオブジェクトのアドレスであるprvalue式です。クラスthisのメンバー関数のXの型はX*です。

thisは、クラス独自のコード内でnon-staticメンバー変数またはメンバー関数を参照するたびに実際に暗黙的です。また、コンパイラーは、実行時に関数または変数を実際のオブジェクトに結び付ける必要があるため、(暗黙的または明示的に)必要です。

たとえば、メンバー関数内でパラメーターとメンバー変数を明確に区別する必要がない限り、明示的に使用することはめったに役立ちません。それ以外の場合、それなしでは、コンパイラーはパラメーターでメンバー変数をシャドーします( Coliruでライブを参照 )。

28
JBL

thisは、非静的メソッドを使用している場合は常に存在する必要があります。明示的に使用するかどうかにかかわらず、現在のインスタンスへの参照が必要です。これがthisが提供するものです。

どちらの場合も、thisポインターを介してメモリにアクセスします。場合によっては省略できます。

17

これは、 アセンブリレベルで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のような最新の命令セットでは、このような古いコンパイラで見逃された最適化を期待してください。

16
Peter Cordes

コンパイル後、すべてのシンボルは単なるアドレスであるため、実行時の問題になることはありません。

thisを使用しなかった場合でも、メンバーシンボルは現在のクラスのオフセットにコンパイルされます。

nameがC++で使用される場合、次のいずれかになります。

  • グローバル名前空間(::nameなど)、現在の名前空間、または使用されている名前空間(using namespace ...が使用されている場合)
  • 現在のクラスで
  • ローカル定義、上のブロック
  • 現在のブロックのローカル定義

したがって、コードを記述するとき、コンパイラは、シンボル名を検索する方法で、現在のブロックからグローバル名前空間までそれぞれをスキャンする必要があります。

this->nameを使用すると、コンパイラはnameの検索を絞り込んで現在のクラススコープでのみ検索します。つまり、ローカル定義をスキップし、クラススコープで見つからない場合は検索しません。グローバルスコープ。

10
SHR

以下は、実行時に「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;
}
7
Helmut Zeisel

あなたのマシンはクラスメソッドについて何も知りません、それらは内部の通常の機能です。したがって、常に現在のオブジェクトにポインターを渡すことでメソッドを実装する必要があります。これは、C++では暗黙的です。つまり、T Class::method(...)T Class_Method(Class* this, ...)の単なる構文糖です。

PythonやLuaのような他の言語はそれを明示的にすることを選択し、Vulkanのような最新のオブジェクト指向C APIは(OpenGLとは異なり)同様のパターンを使用します。

7
Trass3r

通常、メンバー変数または関数を参照するたびに使用するためです。

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で登録または登録しても、「ポインタではありません」。

5
Agent_L

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

(明らかに、そのようなコードを書くことは推奨されません。)

3
Szak1

コンパイラーが、動的バインディングではなく静的バインディングで呼び出されるメンバー関数をインライン化すると、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ではない)、xthis->x、および(*this).xはすべて同じことを意味します。

3
Davislor

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の同等のものより読みやすいためです。

  1. 非標準レイアウト:クラスが標準レイアウトの場合、インスタンスの最初のデータメンバーのアドレスはインスタンス自体のアドレスとまったく同じです。したがって、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
    
  2. 仮想ベースクラス:最も派生したクラスの後に続く仮想ベースのため、仮想から継承されたメンバー関数に提供される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の継承されたVDerivedVBase自体とは異なる開始アドレスを持つため、基本クラスのメンバー関数には異なる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. 複数の継承:予想されるように、複数の継承は、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
    

好奇心が強い場合は、 この小さなプログラム を自由にいじって、複数回実行した場合や、値が異なる場合がある場合にアドレスがどのように変化するかを見てくださいあなたは期待するかもしれません。

3
Justin Time

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();
2
Alexander