Может ли указатель 'this' отличаться от указателя объекта?

Недавно я встретил эту странную функцию в каком-то классе:

void* getThis() {return this;}

И далее в коде он иногда используется так: bla->getThis() (где bla - это указатель на объект класса, в котором эта функция определена.) И я не могу понять, к чему это может быть полезно. Есть ли ситуация, когда указатель на объект будет отличаться от объекта this (где bla != bla->getThis())?

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

Ответы

Ответ 1

Конечно, значения указателя могут быть разными! Ниже пример, демонстрирующий проблему (вам может потребоваться использовать derived1 в вашей системе вместо derived2, чтобы получить разницу). Дело в том, что указатель this обычно корректируется, когда задействовано виртуальное, множественное наследование. Это может быть редкий случай, но это происходит.

Одним из возможных вариантов использования этой идиомы является возможность восстановления объектов известного типа после их хранения как void const* (или void*, правильность const здесь не имеет значения): если у вас есть сложной иерархии наследования, вы не можете просто нарисовать какой-либо нечетный указатель на void* и надеяться, что сможете восстановить его в исходном типе! То есть, чтобы легко получить, например, указатель на base (из приведенного ниже примера) и преобразовать его в void*, вы бы вызвали p->getThis(), что намного проще static_cast<base*>(p) и получить void*, которые можно безопасно отнести к base* с помощью static_cast<base*>(v): вы можете отменить неявное преобразование, но только если вы вернетесь к тому типу, откуда пришел исходный указатель. То есть static_cast<base*>(static_cast<void*>(d)), где d является указателем на объект типа, полученного из base, является незаконным, но static_cast<base*>(d->getThis()) является законным.

Теперь, почему адрес меняется в первую очередь? В примере base является виртуальным базовым классом из двух производных классов, но может быть и больше. Все подобъекты, класс которых фактически наследуется от base, будут совместно использовать один общий объект base в объекте другого производного класса (concrete в приведенном ниже примере). Местоположение этого субобъекта base может быть различным относительно соответствующего производного подобъекта в зависимости от того, как упорядочиваются разные классы. В результате указатель на объект base обычно отличается от указателей на подобъекты классов, фактически наследующих от base. Соответствующее смещение будет вычисляться во время компиляции, когда это возможно, или из-за чего-то вроде vtable во время выполнения. Смещения корректируются при преобразовании указателей вдоль иерархии наследования.

#include <iostream>

struct base
{
    void const* getThis() const { return this; }
};

struct derived1
    : virtual base
{
    int a;
};

struct derived2
    : virtual base
{
    int b;
};

struct concrete
    : derived1
    , derived2
{
};

int main()
{
    concrete c;
    derived2* d2 = &c;
    void const* dptr = d2;
    void const* gptr = d2->getThis();
    std::cout << "dptr=" << dptr << " gptr=" << gptr << '\n';
}

Ответ 2

Нет. Да, в ограниченных обстоятельствах.

Похоже, что это что-то вдохновило Smalltalk, в котором все объекты имеют метод yourself. Вероятно, есть некоторые ситуации, в которых это делает очиститель кода. Как отмечается в комментариях, это выглядит как странный способ даже реализовать эту идиому в С++.

В вашем конкретном случае я хотел бы использовать фактические способы использования метода, чтобы узнать, как он используется.

Ответ 3

У вашего класса может быть пользовательский operator& (поэтому &a может не возвращать this of a). Поэтому std:: addressof существует.

Ответ 4

Я столкнулся с чем-то подобным этим много (много много) лет назад. Если я правильно помню, это было необходимо, когда класс манипулировал другими экземплярами одного и того же класса. Одним из примеров может быть контейнерный класс, который может содержать свой собственный тип/(класс?).

Ответ 5

Это может быть способ переопределить это ключевое слово. Допустим, что у вас есть пул памяти, который полностью инициализирован в начале вашей программы, например, вы знаете, что в любое время вы можете иметь дело с максимальным количеством сообщений 50, CMessage. Вы создаете пул размером 50 * sizeof (CMessage) (каким бы он ни был этот класс), а CMessage реализует функцию getThis.

Таким образом, вместо переопределения нового ключевого слова вы просто переопределяете "this", обращаясь к пулу. Это также может означать, что объект может быть определен в разных пространствах памяти, скажем, на SRAM, в режиме загрузки, а затем на SDRAM.

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