C++ 3d.Комментарии

       

Указатели на члены


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

Это утверждение, вообще говоря, неверно и я вам советую никогда так не поступать. Сейчас покажу почему.

Прежде всего, стоит отметить, что в C++ вы не сможете прямо вывести значение указателя на член: struct S { int i; void f(); };

void g() { cout<<&S::i; // ошибка: operator<< не реализован для типа int S::* cout<<&S::f; // ошибка: operator<< не реализован для типа void (S::*)() }

Это довольно странно. Andrew Koenig пишет по этому поводу, что дело не в недосмотре разработчиков библиотеки ввода/вывода, а в том, что не существует переносимого способа для вывода чего-либо содержательного (кстати, я оказался первым, кто вообще об этом спросил, так что проблему определенно нельзя назвать злободневной). Мое же мнение состоит в том, что каждая из реализаций вполне способна найти способ для вывода более-менее содержательной информации, т.к. в данном случае даже неидеальное решение -- это гораздо лучше, чем вообще ничего.

Поэтому для иллюстрации внутреннего представления указателей на члены я написал следующий пример: #include <string.h> #include <stdio.h>

struct S { int i1; int i2;

void f1(); void f2();

virtual void vf1(); virtual void vf2(); };

const int SZ=sizeof(&S::f1);

union { unsigned char c[SZ]; int i[SZ/sizeof(int)]; int S::* iptr; void (S::*fptr)(); } hack;

void printVal(int s) { if (s%sizeof(int)) for (int i=0; i<s; i++) printf(" %02x", hack.c[i]); else for (int i=0; i<s/sizeof(int); i++) printf(" %0*x", sizeof(int)*2, hack.i[i]);

printf("\n"); memset(&hack, 0, sizeof(hack)); }



int main() { printf("sizeof(int)=%d sizeof(void*)=%d\n", sizeof(int), sizeof(void*));

hack.iptr=&S::i1; printf("sizeof(&S::i1 )=%2d value=", sizeof(&S::i1)); printVal(sizeof(&S::i1));

hack.iptr=&S::i2; printf("sizeof(&S::i2 )=%2d value=", sizeof(&S::i2)); printVal(sizeof(&S::i2));


hack.fptr=&S::f1; printf("sizeof(&S::f1 )=%2d value=", sizeof(&S::f1)); printVal(sizeof(&S::f1));

hack.fptr=&S::f2; printf("sizeof(&S::f2 )=%2d value=", sizeof(&S::f2)); printVal(sizeof(&S::f2));

hack.fptr=&S::vf1; printf("sizeof(&S::vf1)=%2d value=", sizeof(&S::vf1)); printVal(sizeof(&S::vf1));

hack.fptr=&S::vf2; printf("sizeof(&S::vf2)=%2d value=", sizeof(&S::vf2)); printVal(sizeof(&S::vf2)); }

void S::f1() {} void S::f2() {}

void S::vf1() {} void S::vf2() {}

Существенными для понимания местами здесь являются объединение hack, используемое для преобразования значения указателей на члены в последовательность байт (или целых), и функция printVal(), печатающая данные значения.

Я запускал вышеприведенный пример на трех компиляторах, вот результаты: sizeof(int)=4 sizeof(void*)=4 sizeof(&S::i1 )= 8 value= 00000005 00000000 sizeof(&S::i2 )= 8 value= 00000009 00000000 sizeof(&S::f1 )=12 value= 004012e4 00000000 00000000 sizeof(&S::f2 )=12 value= 004012ec 00000000 00000000 sizeof(&S::vf1)=12 value= 004012d0 00000000 00000000 sizeof(&S::vf2)=12 value= 004012d8 00000000 00000000

sizeof(int)=4 sizeof(void*)=4 sizeof(&S::i1 )= 4 value= 00000001 sizeof(&S::i2 )= 4 value= 00000005 sizeof(&S::f1 )= 8 value= ffff0000 004014e4 sizeof(&S::f2 )= 8 value= ffff0000 004014f4 sizeof(&S::vf1)= 8 value= 00020000 00000008 sizeof(&S::vf2)= 8 value= 00030000 00000008

sizeof(int)=4 sizeof(void*)=4 sizeof(&S::i1 )= 4 value= 00000004 sizeof(&S::i2 )= 4 value= 00000008 sizeof(&S::f1 )= 4 value= 00401140 sizeof(&S::f2 )= 4 value= 00401140 sizeof(&S::vf1)= 4 value= 00401150 sizeof(&S::vf2)= 4 value= 00401160

Прежде всего в глаза бросается то, что несмотря на одинаковый размер int и void*, каждая из реализаций постаралась отличиться в выборе представления указателей на члены, особенно первая. Что же мы можем сказать еще?




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

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

    А теперь самое интересное -- указатели на виртуальные функции-члены. Как вы можете видеть, только у одного из трех компиляторов они получились похожими на "передаваемые" -- у второго.

    Итак, указатели на виртуальные функции-члены можно безопасно передавать в другое адресное пространство чрезвычайно редко. И это правильно! Дело в том, что в определение C++ закралась ошибка: указатели на обычные и виртуальные члены должны быть разными типами. Только в этом случае можно обеспечить оптимальность реализации.

    Указатели на функции-члены во втором компиляторе реализованы неоптимально, т.к. иногда они содержат указатель на "обычную" функцию (ffff0000 004014e4), а иногда -- индекс виртуальной функции (00020000 00000008). В результате чего, вместо того, чтобы сразу произвести косвенный вызов функции, компилятор проверяет старшую часть первого int, и если там стоит -1 (ffff), то он имеет дело с обычной функцией членом, иначе -- с виртуальной. Подобного рода проверки при каждом вызове функции-члена через указатель вызывают ненужные накладные расходы.

    Внимательный читатель должен спросить: "Хорошо, пусть они всегда содержат обычный указатель на функцию, но как тогда быть с указателями на виртуальные функции? Ведь мы не можем использовать один конкретный адрес, так как виртуальные функции принято замещать в производных классах." Правильно, дорогой читатель! Но выход есть, и он очевиден: в этом случае компилятор автоматически генерирует промежуточную функцию-заглушку.

    Например, следующий код: struct S { virtual void vf() { /* 1 */ } void f () { /* 2 */ } };

    void g(void (S::*fptr)(), S* sptr) { (sptr->*fptr)(); }

    int main() { S s; g(S::vf, &s); g(S::f , &s); }

    превращается в псевдокод: void S_vf(S *const this) { /* 1 */ } void S_f (S *const this) { /* 2 */ }

    void S_vf_stub(S *const this) { // виртуальный вызов функции S::vf() (this->vptr[index_of_vf])(this); }

    void g(void (*fptr)(S *const), S* sptr) { fptr(sptr); }

    int main() { S s; g(S_vf_stub, &s); // обратите внимание: не S_vf !!! g(S_f , &s); }

    А если бы в C++ присутствовал отдельный тип "указатель на виртуальную функцию-член", он был бы представлен простым индексом виртуальной функции, т.е. фактически простым size_t, и генерации функций-заглушек (со всеми вытекающими потерями производительности) было бы можно избежать. Более того, его, как и указатель на данные-член, всегда можно было бы передавать в другое адресное пространство.


    Содержание раздела