Указатель "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 кстати.

Ответы

Ответ 1

Так это указатель просто вещь времени компиляции, а не фактический указатель?

Это очень много времени выполнения. Он относится к объекту, для которого вызывается функция-член, естественно, этот объект может существовать во время выполнения.

Что такое время компиляции, так это то, как работает поиск имени. Когда компилятор встречает x = X он должен выяснить, что это за назначенный x. Так что он ищет его и находит переменную-член. Поскольку this->x и x относятся к одной и той же вещи, естественно, вы получаете одинаковый вывод сборки.

Ответ 2

Это фактический указатель, как указано в стандарте (§12.2.2.1):

В теле нестатической (12.2.1) функции-члена ключевое слово this является выражением prvalue, значением которого является адрес объекта, для которого вызывается функция. Тип this в функции-члене класса X - X*.

this фактически неявно каждый раз, когда вы ссылаетесь на нестатическую переменную - член или функцию-член в собственном коде класса. Он также необходим (когда он неявный или явный), потому что компилятору необходимо привязать функцию или переменную к реальному объекту во время выполнения.

Использование его в явном виде редко бывает полезным, если, например, вам не нужно, например, устранять неоднозначность между параметром и переменной-членом в функции-члене. В противном случае, без него компилятор будет скрывать переменную-член с параметром (см. В прямом эфире на Coliru).

Ответ 3

this всегда должно существовать, когда вы используете нестатический метод. Независимо от того, используете ли вы его явно или нет, у вас должна быть ссылка на текущий экземпляр, и это то, что вам this дает.

В обоих случаях вы собираетесь обращаться к памяти через указатель this. Просто вы можете опустить его в некоторых случаях.

Ответ 4

Это почти дубликат Как объекты работают в x86 на уровне сборки? , где я комментирую вывод asm некоторых примеров, включая показ, в каком регистре был передан this указатель.


В asm this работает точно так же, как скрытый первый аргумент, так что и функция-член foo::add(int) и не член add который принимает явную компиляцию foo* first 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;
}

В проводнике компилятора Godbolt, компилируя для x86-64 с помощью System V ABI (первый аргумент в RDI, второй в RSI), мы получаем:

# 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 года, поэтому в нем отсутствовало почти десятилетие улучшений оптимизатора и сообщений об ошибках. Серия gcc7 уже давно и стабильна. Ожидайте пропущенные оптимизации с таким старым компилятором, особенно для современных наборов команд, таких как AVX.

Ответ 5

После компиляции каждый символ является просто адресом, поэтому он не может быть проблемой во время выполнения.

Любой символ члена в любом случае компилируется в смещение в текущем классе, даже если вы не использовали this.

Когда name используется в C++, оно может быть одним из следующих.

  • В глобальном пространстве имен (например, ::name), или в текущем пространстве имен, или в используемом пространстве имен (при using namespace... было использовано)
  • В текущем классе
  • Локальное определение, в верхнем блоке
  • Локальное определение, в текущем блоке

Поэтому, когда вы пишете код, компилятор должен сканировать каждый таким образом, чтобы искать имя символа, начиная с текущего блока и вплоть до глобального пространства имен.

Использование this->name помогает компилятору сузить поиск по name чтобы искать его только в текущей области класса, то есть пропускает локальные определения, и, если он не найден в области класса, не ищите его в глобальной области.

Ответ 6

Вот простой пример того, как "это" может быть полезно во время выполнения:

#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

Ваша машина ничего не знает о методах класса, они нормальные функции под капотом. Следовательно, методы должны быть реализованы, всегда передавая указатель на текущий объект, он просто неявный в C++, то есть T Class::method(...) является просто синтаксическим сахаром для T Class_Method(Class* this,...)

Другие языки, такие как Python или Lua, делают это явным, и современные объектно-ориентированные C-API, такие как Vulkan (в отличие от OpenGL), используют аналогичную модель.

Ответ 8

так как я обычно использую его каждый раз, когда я ссылаюсь на переменную-член или функцию.

Вы всегда используете this когда ссылаетесь на переменную-член или функцию. Другого способа связаться с участниками просто нет. Единственный выбор - неявная или явная запись.

Давайте вернемся, чтобы посмотреть, как это было сделано до this чтобы понять, что this такое.

Без ООП:

struct A {
    int x;
};

void foo(A* that) {
    bar(that->x)
}

С ООП, но писать this явно

struct A {
    int x;

    void foo(void) {
        bar(this->x)
    }
};

используя более короткую запись:

struct A {
    int x;

    void foo(void) {
        bar(x)
    }
};

Но разница только в исходном коде. Все они скомпилированы в одно и то же. Если вы создаете метод-член, компилятор создаст для вас аргумент-указатель и назовет его "this". Если вы опускаете this-> при обращении к члену, компилятор достаточно умен, чтобы вставлять его для вас большую часть времени. Это. Разница лишь в том, что в источнике меньше букв.

Запись this явно имеет смысл, когда есть неоднозначность, а именно другая переменная, названная так же, как ваша переменная-член:

struct A {
    int x;

    A(int x) {
        this->x = x
    }
};

В некоторых случаях, например __thiscall, код OO и код, отличный от OO, могут заканчиваться битами по-разному в asm, но всякий раз, когда указатель передается в стек, а затем оптимизируется в регистр или в ECX с самого начала, он этого не делает, "не указатель ".

Ответ 9

если компилятор встроит функцию-член, которая вызывается с помощью статического, а не динамического связывания, он может оптимизировать указатель 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    [email protected]

Это вызов 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

Вы помните из предыдущего примера, что %rdi на x86-64 является первым аргументом функции, неявным указателем this для вызова e.foo(). Помещение в круглые скобки (%rdi) означает поиск переменной в этом месте. (Поскольку в example экземпляра единственными данными являются x, &e.x в этом случае совпадает с &e в этом случае.) Перемещение содержимого в %eax устанавливает возвращаемое значение.

В этом случае компилятору нужен неявный аргумент this для foo(/* example* this */) чтобы иметь возможность найти &e и, следовательно, &e.x Фактически, внутри функции-члена (которая не является static), x, this->x и (*this).x все означают одно и то же.

Ответ 10

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

(Очевидно, что писать такой код не рекомендуется.)

Ответ 11

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

Эти значения не гарантированы и могут легко меняться от одного выполнения к другому; это легче всего наблюдать при создании и тестировании программы, используя инструменты сборки.


Механически это похоже на скрытый параметр, добавляемый в начало списка аргументов каждой функции-члена; xf() cv можно рассматривать как особый вариант f(cv X* this), хотя и с другим форматом по лингвистическим причинам. Фактически, в последнее время Stroustrup и Sutter предложили унифицировать синтаксис вызова xf(y) и f(x, y), что сделало бы это неявное поведение явным лингвистическим правилом. К сожалению, он был встречен с опасениями, что это может вызвать несколько нежелательных сюрпризов для разработчиков библиотек, и, таким образом, еще не реализовано; Насколько мне известно, самое последнее предложение является совместным предложением, чтобы f(x,y) могла использовать xf(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::[email protected]:
            | &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 унаследованная VDerived VBase имеет начальный адрес, VDerived самого VDerived.

    class VDerived  size(8):
            +---
     0      | {vbptr}
            +---
            +--- (virtual base VBase)
            +---
    VDerived::[email protected]:
     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? Это происходит из-за небольшой хитрости компилятора, называемой "корректором 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::[email protected]@:
            | &Derived_meta
            |  0
     0      | &Derived::address 
    Derived::[email protected]@:
            | -16
     0      | &thunk: this-=16; goto Derived::address 
    Derived::address this adjustor: 0
    

Если вам интересно, не стесняйтесь возиться с этой маленькой программой, посмотреть, как меняются адреса, если вы запускаете ее несколько раз, или в тех случаях, когда она может иметь значение, отличное от ожидаемого.

Ответ 12

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