Указатель "this" - это просто время компиляции? - PullRequest
0 голосов
/ 12 ноября 2018

Я спросил себя, можно ли использовать указатель 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 между прочим.

Ответы [ 12 ]

0 голосов
/ 15 ноября 2018

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.

  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::$vftable@:
            | &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 наследуется 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
    
  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::$vftable@Base1@:
            | &Derived_meta
            |  0
     0      | &Derived::address 
    Derived::$vftable@Base2@:
            | -16
     0      | &thunk: this-=16; goto Derived::address 
    Derived::address this adjustor: 0
    

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

0 голосов
/ 13 ноября 2018

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

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

0 голосов
/ 14 ноября 2018

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();
0 голосов
/ 13 ноября 2018

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

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

0 голосов
/ 13 ноября 2018

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

Вы всегда используете 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-> при обращении к члену, компилятор достаточно умен, чтобы вставлять его для вас большую часть времени. Вот и все. Разница лишь в том, что в источнике меньше букв на 6 букв.

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

struct A {
    int x;

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

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

0 голосов
/ 12 ноября 2018

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


В asm this работает точно так же, как скрытый первый аргумент , поэтому функция-член foo::add(int) и не член add, который принимает явный foo* первый arg компилируется точно так же, как.

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.

0 голосов
/ 13 ноября 2018

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

Вы помните из предыдущего примера, что %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 все означают одно и то же.

0 голосов
/ 12 ноября 2018

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

#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;
}
0 голосов
/ 12 ноября 2018

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

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

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

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

0 голосов
/ 12 ноября 2018

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

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

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

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

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

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

...