Указатели на функции-члены и фантомные классы
Я возился с указателями на функции-члены по отношению к предыдущему вопросу. В приведенном ниже коде я вызываю методы для класса (B), которые изменяют переменную (число) в нем, но я никогда не создаю экземпляр этого класса. Почему это работает?
#include <iostream>
#include <string>
#include <map>
class A;
typedef int (A::*MEMFUNC)(int, int);
#define HANDLER(aclass, aproc) (MEMFUNC)(&aclass::aproc)
enum
{
ADD=1,
MUL,
SUB,
DIV
};
class B
{
int count;
public:
B() : count(0) {}
~B() {}
int multiply(int x, int y) { count++; return x*y*count; }
int divide(int x, int y) { count++; if (y!=0) return (x/y)*count; else return 0; }
};
class A
{
std::map< int, MEMFUNC > funcs;
public:
A() { AddLocals(); }
~A() {}
int CallLocal(int nID, int x, int y)
{
MEMFUNC f = funcs[nID];
if (f) return (this->*f)(x, y);
else return 0;
}
void AddLocals()
{
Add(ADD, HANDLER(A, plus));
Add(MUL, HANDLER(B, multiply));
Add(SUB, HANDLER(A, subtract));
Add(DIV, HANDLER(B, divide));
}
void Add(int nID, MEMFUNC f) { funcs[nID] = f; }
int plus(int x, int y) { return x+y; }
int subtract(int x, int y) { return x-y; }
};
int main()
{
A aA;
int a,b,c,d;
a = aA.CallLocal(ADD,8,2);
b = aA.CallLocal(MUL,8,2);
c = aA.CallLocal(SUB,8,2);
d = aA.CallLocal(DIV,8,2);
std::cout << "a = " << a << "\n"
<< "b = " << b << "\n"
<< "c = " << c << "\n"
<< "d = " << d << "\n";
return 0;
}
(извините, я снова, но указатели на функции-члены вызывают у меня зуд)
4 ответа
Результатом является просто неопределенное поведение. Например, я понимаю, что b = 2083899728
а также d = -552766888
,
Постоянная вещь, которой вы манипулируете, - это, скорее всего, байты int в экземпляре карты A (потому что, если объект действительно был B, то это смещение, где count
член будет расположен.
В моей реализации stdlib первым элементом map является функция сравнения, в данном случае экземпляр std::less<int>
, Его размер равен 1, но после этого должны быть неиспользованные байты заполнения для выравнивания других элементов карты. То есть (по крайней мере) первые четыре байта этого экземпляра std::map
содержит только мусор, который ни для чего не используется (std::less не имеет элементов данных и не хранит состояние, он просто занимает место на карте). Это объясняет, почему код не дает сбоя - он изменяет часть экземпляра карты, которая не влияет на функционирование карты.
Добавить больше членов данных в B, прежде чем count
, и сейчас count++
повлияет на важные части внутреннего представления карты, и вы можете получить сбой.
Ваш актерский состав в HANDLER
macro def говорит компилятору: "Заткнись! Я знаю, что делаю!".
Итак, компилятор отключается.
У вас все еще есть неопределенное поведение, но одно свойство UB - то, что в некоторых случаях он делает то, что вы наивно ожидаете, или то, что вы хотите, чтобы он делал.
Но не удивляйтесь, если такой код вылетает, вызывает сбои или таинственный неверный результат в явно не связанном коде.
Или, например, заставляет носовых демонов вылетать из носа.
Ура & hth.
C-casting позволяет вам избегать всех видов ужасного поведения, но не значит, что это нормально, просто не делайте этого.
Избавьтесь от своего макроса полностью и не разыгрывайте. Вы можете, вероятно, использовать boost::function и boost:: bind, чтобы получить поведение, которое вы действительно хотите.
Ваш код вызывает неопределенное поведение, пытаясь вызвать член класса B, используя объект класса A. Мы можем попытаться объяснить, как компилятор может прийти к наблюдаемому вами поведению, но нет никакой гарантии, что вы получите то же самое. поведение, если вы что-то измените (добавьте / удалите элемент, измените настройки компилятора или используйте другой компилятор).
С актерами в HANDLER
macro, вы говорите компилятору не предупреждать вас об использовании несовместимых типов, а просто делать так, как вы говорите. В этом случае вы указываете компилятору переинтерпретировать адрес члена любого класса как адрес члена класса А.
Когда вы позже попытаетесь позвонить, например, B::multiply
эта функция не знает, что она не работает с объектом класса B, поэтому она с радостью забьет байты aA
это будет соответствовать B::count
член, если бы это было B
объект. Скорее всего, эти байты фактически используются A::funcs
но, видимо, не для чего-то критического. Если вы измените класс A на:
class A
{
int count;
std::map< int, MEMFUNC > funcs;
public:
A() : count(0) { AddLocals(); }
~A() {}
int CallLocal(int nID, int x, int y)
{
MEMFUNC f = funcs[nID];
if (f) return (this->*f)(x, y);
else return 0;
}
int Count()
{
return count;
}
void AddLocals()
{
Add(ADD, HANDLER(A, plus));
Add(MUL, HANDLER(B, multiply));
Add(SUB, HANDLER(A, subtract));
Add(DIV, HANDLER(B, divide));
}
void Add(int nID, MEMFUNC f) { funcs[nID] = f; }
int plus(int x, int y) { return x+y; }
int subtract(int x, int y) { return x-y; }
};
затем распечатать результат aA.Count()
в разных местах может показать эффект.
Компилятор вызывает ожидаемую функцию, потому что это не виртуальные функции-члены.
Единственное различие между не-членами-функциями и не-виртуальными функциями-членами заключается в скрытом аргументе, который передает this
указатель на функцию-член. Таким образом, если вы возьмете адрес не-виртуальной функции-члена, вы получите фиксированный адрес, который отличается для каждой функции.
Если бы функции-члены были виртуальными, то компилятор, скорее всего, вернул бы индекс в v-таблицу в качестве указателя на эту функцию (вместе с каким-то указанием на то, что это смещение v-таблицы). Затем код может определить на месте вызова, может ли он выполнить прямой вызов функции-члена или нужно ли выполнить косвенный вызов через v-таблицу объекта, для которого вызывается функция.