Что такое `int foo::*bar::*`?

Отличительной особенностью C++ является то, что он позволяет создавать переменные типа указатель на член. Наиболее распространенным вариантом использования, по-видимому, является получение указателя на метод:

struct foo
{
    int x() { return 5; }
};

int (foo::*ptr)() = &foo::x;
foo myFoo;
cout << (myFoo.*ptr)() << '\n'; // prints "5"

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

struct foo
{
    int y;
};

int foo::*ptr = &foo::y;
foo myFoo;
myFoo.*ptr = 5;
cout << myFoo.y << '\n'; // prints "5"

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

struct foo
{
    int y;
};

struct bar
{
    foo aFoo;
};

int bar::*foo::*ptr;

Этот фактически компилируется.

Однако я понятия не имею, как назначить ему что-нибудь полезное. Ни одна из следующих работ:

int bar::*foo::*ptr = &bar::foo::y; // no member named "foo" in "bar"
int bar::*foo::*ptr = &bar::aFoo::y; // no member named "aFoo" in "bar" (??)
int bar::*foo::*ptr = &foo::y; // can't init 'int bar::*foo::*' with 'int foo::*'

Кроме того, в соответствии с ошибкой, которую это генерирует, кажется, что этот тип не совсем то, что я имею в виду:

int bar::*foo::*ptr = nullptr;
bar myBar;
myBar.*ptr = 4; // pointer to member type ‘int bar::*’ incompatible
                // with object type ‘bar’

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

Кто-нибудь, пожалуйста, объясните мне, что такое int bar::*foo::* на самом деле? Почему gcc сообщает мне, что указатель на элемент bar несовместим с объектом bar? Как бы я использовал int bar::*foo::* и как бы я построил правильный?


person zneak    schedule 05.10.2014    source источник
comment
Это похоже на окольный путь, пытающийся сделать то, что decltype() с соответствующими шаблонами уже доставляет?   -  person slashmais    schedule 05.10.2014
comment
Возможно, я недостаточно разбираюсь в предмете, но если это поможет, int bar::*foo::*ptr = *new decltype(ptr); компилируется, но я не знаю, что он делает, и ничего не могу сделать с результатом. Это тоже утечка памяти, но иногда памятью приходится жертвовать во имя науки.   -  person IllusiveBrian    schedule 05.10.2014
comment
Блин, когда авторы компилятора реализовали это, я клянусь, они много ругались о том, кто, черт возьми, когда-либо будет это использовать.   -  person marczellm    schedule 06.10.2014
comment
ваш самый первый пример кода имеет синтаксическую ошибку. это должно быть: int (foo::*ptr)() = &foo::x;   -  person null0    schedule 06.10.2014
comment
Это может быть похоже на отражение в Java... очень полезно для некоторых эзотерических вещей, таких как реализация библиотек, таких как BeanUtils, но почти никогда не является хорошей идеей для использования в других целях.   -  person trognanders    schedule 11.11.2014


Ответы (5)


Вот "действительный" способ инициализации такого чудовища:

struct bar;

struct foo
{
    int y;    
    int bar::* whatever;
};

struct bar
{
    foo aFoo;
};

int bar::* foo::* ptr = &foo::whatever;

Как мы видим, ptr является указателем на элемент foo (foo::*, читается справа налево), где этот элемент сам является указателем на член bar (bar::*), где этот элемент является целым числом.

Как бы я использовал int bar::* foo::*

Вы бы не стали, надеюсь! Но если вы находитесь под принуждением, попробуйте это!

struct bar
{
    foo aFoo;

    int really;
};

int bar::* foo::* ptr = &foo::whatever;
foo fleh;
fleh.whatever = &bar::really;
bar blah;
blah.*(fleh.*ptr) = 42;
std::cout << blah.really << std::endl;
person John Zwinck    schedule 05.10.2014
comment
Я не уверен, что понимаю, как это анализируется, но это будет не первый раз, когда я не понимаю, как анализируется С++. - person zneak; 05.10.2014
comment
Это не всегда чудовище. Рассмотрим случай с итератором foo, которому по какой-то причине нужно сослаться на свой контейнер и панель контейнера, содержащую, например, итератор head. - person Michael; 07.10.2014

Это будет указатель на член данных, который сам является указателем на член данных (член int из bar).

Не спрашивайте меня, для чего это вообще полезно - голова немного кружится :)

РЕДАКТИРОВАТЬ: Вот полный пример этого в действии:

#include <iostream>

struct bar {
    int i;
};

struct foo {
    int bar::* p;
};

int main()
{
    bar b;
    b.i = 42;

    foo f;
    f.p = &bar::i;

    int bar::*foo::*ptr = &foo::p;
    std::cout << (b.*(f.*ptr));
}

Выход, конечно, 42.

Это может быть еще интереснее — вот несколько указателей на функции-члены, которые возвращают указатели на функции-члены:

#include <iostream>

struct bar {
    int f_bar(int i) { return i; };
};

struct foo {
    int(bar::*f_foo())(int)
    {
        return &bar::f_bar;
    }
};

int main()
{
    int(bar::*((foo::*ptr)()))(int) = &foo::f_foo;

    bar b;
    foo f;

    std::cout << (b.*((f.*ptr)()))(42);
}
person jrok    schedule 05.10.2014
comment
Как только я подумал, что начинаю понимать некоторые из этих утверждений, ваш второй пример разрушил мои надежды. - person Cengiz Can; 09.10.2014
comment
Я понимаю, почему второй пример разбил ваши надежды :). - person hlide; 13.02.2016
comment
1) bar определяет метод f_bar, имеющий целочисленный аргумент и возвращающий целое число. Пока легко понять. 2) foo определяет метод f_foo, не имеющий аргументов и возвращающий указатель на метод bar, имеющий целочисленный аргумент и возвращающий целое число. Менее легко читать, я признаю. 3) main first определяет локальный 'ptr' как указатель на метод foo, не имеющий аргумента и возвращающий указатель на метод bar, имеющий целочисленный аргумент и возвращающий целое число, и присваивает его методу f_foo foo. Боже, как долго это описывать! продолжение следует ! - person hlide; 14.02.2016
comment
4) Поскольку f_bar и f_foo являются методами, вам нужно определить b и f, чтобы вы могли вызывать (f.*ptr)() для возврата &bar::f_bar. Итак, std::cout << (b.*((f.*ptr)()))(42); в основном такой же, как std::cout << (b.*(&bar::f_bar))(42);. 5) Ой, подождите! вызов '(b.*(&bar::f_bar))(42);' это то же самое, что и вызов 'b.f_bar(42);` ! Итак, вы действительно выводите 42. - person hlide; 14.02.2016

Давайте разберем объявление int bar::*foo::*ptr;.

§8.3.3 [dcl.mptr]/p1:

В объявлении T D, где D имеет форму

nested-name-specifier * attribute-specifier-seq_opt cv-qualifier-seq_opt D1

а спецификатор вложенного имени обозначает класс, а тип идентификатора в объявлении T D1 — «список-типов-производных-деклараторов T», тогда тип идентификатора D — « список-производных-деклараторов cv-qualifier-seq указатель на член класса вложенный-спецификатор-имени типа T».

  • Шаг 1: Это объявление приведенной выше формы, где T = int, вложенный-спецификатор-имени = bar:: и D1 = foo::* ptr. Сначала мы смотрим на объявление T D1 или int foo::* ptr.

  • Шаг 2: Мы снова применяем то же правило. int foo::* ptr — это объявление приведенной выше формы, где T = int, описатель вложенного имени = foo:: и D1 = ptr. Очевидно, тип идентификатора в int ptr — «int», поэтому тип идентификатора ptr в объявлении int foo::* ptr — это «указатель на член класса foo типа int».

  • Шаг 3. Возвращаемся к исходному объявлению; тип идентификатора в T D1(int foo::* ptr) — «указатель на член класса foo типа int» на шаге 2, поэтому список-производных-деклараторов — это «указатель на член класса foo». типа". Подстановка говорит нам, что это объявление объявляет ptr "указателем на член класса foo типа указателя на член класса bar типа int".

Надеюсь, вам никогда не понадобится использовать такое чудовище.

person T.C.    schedule 05.10.2014
comment
Были бы отличия, если бы мы объявили int bar::*(foo::*ptr) = nullptr; вместо этого? - person ; 05.10.2014
comment
@DmitryFucintv Нет, привязка такая же. - person T.C.; 05.10.2014

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

Использование простого смещения не работает из-за виртуального наследования и тому подобного; в основном может случиться так, что даже в пределах одного типа смещения определенного поля различаются между экземплярами, и поэтому разрешение указателя на член необходимо выполнять во время выполнения. В основном это связано с тем, что в стандарте не указано, как может работать внутренняя компоновка для типов, отличных от POD, поэтому нет способа заставить ее работать статично.

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

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

person Svalorzen    schedule 05.10.2014

Во-первых, чтобы улучшить "читабельность", вы можете использовать круглые скобки (компиляция будет работать):

struct bar;

struct foo
{
    int y;
    int (bar:: *whatever); // whatever is a pointer upon an int member of bar.
};

struct bar
{
    foo aFoo;
};

// ptr is a pointer upon a member of foo which points upon an int member of bar.
int (bar:: *(foo:: *ptr)) = &foo::whatever;

Обратите внимание, что

int (бар :: * что угодно)

эквивалентно

интервал (* что угодно)

с ограничением на принадлежность к бару.

Что касается

int (бар:: *(foo:: *ptr))

, это эквивалентно

интервал (*(*указатель))

с двумя ограничениями на членство в foo и bar.

Они просто указатели. Они не проверяют, действительно ли у bar или foo есть совместимый член, потому что это помешало бы использовать предварительное объявление класса bar, а class bar не проверяет, ссылаются ли другие классы на его члены через указатели. Кроме того, вам также может понадобиться сослаться на непрозрачный класс (то есть иметь панель классов, определенную в отдельном модуле).

А полезность? может быть, для отражения С++ как способ установить/получить значение члена класса через оболочку класса?

template< typename Class, typename Type >
struct ClassMember
{
    using MemberPointer = Type (Class:: *);
    MemberPointer member;
    ClassMember(MemberPointer member) : member(member) {}
    void operator()(Class & object, Type value) { object.*member = value; }
    Type operator()(Class & object)  { return object.*member; }
};

template< typename Class, typename Type > ClassMember< Class, Type > MakeClassMember(Type(Class:: *member))
{
    return ClassMember< Class, Type >(member);
}

struct Rectangle
{
    double width;
    double height;

    Rectangle(double width = 0., double height = 0.) : width(width), height(height) {}
};

int main(int argc, char const *argv[])
{
    auto r = Rectangle(2., 1.);

    auto w = MakeClassMember(&Rectangle::width);
    auto h = MakeClassMember(&Rectangle::height);

    w(r, 3.);
    h(r, 2.);

    printf("Rectangle(%f, %f)\n", w(r), h(r));

    return 0;
}

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

person hlide    schedule 02.01.2015