Указатели на функции-члены и фантомные классы

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

Другие вопросы по тегам