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

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

(извините, снова я, но эти указатели функций-членов вызывают у меня зуд)


person slashmais    schedule 19.10.2010    source источник
comment
Есть ли шанс, что вы могли бы упростить этот код?   -  person Oliver Charlesworth    schedule 19.10.2010
comment
Я взял на себя смелость заменить ваш код гораздо более простым примером. Обязательно откатитесь, если не нравится...   -  person Oliver Charlesworth    schedule 19.10.2010
comment
Это не работает. Просто похоже, что это работает. Есть огромная разница.   -  person jalf    schedule 19.10.2010
comment
@Oli: есть две вещи, на которые я хочу указать в зависимости от ответов: один использует «это» для вызова внешних членов, а другой заключается в том, что увеличенный «счетчик» сохраняет свое значение, что означает, что B каким-то образом создается экземпляр и сохраняется между двумя несвязанными вызовами. Но спасибо за мысль.   -  person slashmais    schedule 19.10.2010
comment
@jalf: работает в Debian с использованием gcc ...   -  person slashmais    schedule 19.10.2010
comment
@slashmais: Да, мой код сохранил все это, но избавился от #define, enum, map, посторонних слоев вызовов функций, деструкторов и т. д. Всегда лучше опубликовать минимальный пример кода.   -  person Oliver Charlesworth    schedule 19.10.2010
comment
@slashmais: Нет, просто похоже, что это работает.   -  person Oliver Charlesworth    schedule 19.10.2010
comment
@Oli: тогда поместите это в ответ и объясните, почему это «похоже, что это работает»   -  person slashmais    schedule 19.10.2010
comment
@slashmais: Мне нет смысла создавать отдельный ответ, потому что ответ Альфа правильный.   -  person Oliver Charlesworth    schedule 19.10.2010
comment
@slashmais: нет, похоже, что это работает в Debian с использованием gcc. Как указывает ответ @Alfs, это поведение undefined. Это означает, что он может иногда делать правильные вещи или, возможно, выглядеть так, будто делает правильные вещи, одновременно искажая другую память где-то, что позже может сбить вас с толку. Но это также означает, что у вас нет гарантии, что она все еще будет работать при следующем запуске вашей программы. Так что это только похоже, что это работает. Он работает только в том случае, если вы можете доверять ему продолжать работать.   -  person jalf    schedule 19.10.2010
comment
Да, вы правы, посетитель, кажется, находит что-то более полезное, чем UB.   -  person slashmais    schedule 19.10.2010


Ответы (4)


Результат - просто неопределенное поведение. Например, я понимаю, что b = 2083899728 и d = -552766888.

Постоянная вещь, которой вы манипулируете, скорее всего, представляет собой целочисленное значение байтов в экземпляре карты A (потому что, если бы объект действительно был B, то это смещение, где был бы расположен элемент count.

В моей реализации stdlib первым членом map является функция сравнения, в данном случае экземпляр std::less<int>. Его размер равен 1, но после этого должны быть неиспользуемые байты заполнения, чтобы выровнять другие члены карты. То есть (по крайней мере) первые четыре байта этого экземпляра std::map содержат просто мусор, который ни для чего не используется (std::less не имеет элементов данных и не хранит состояние, он просто занимает место на карте ). Это объясняет, почему код не дает сбоев — он изменяет часть экземпляра карты, которая не влияет на работу карты.

Добавьте больше элементов данных в B перед count, и теперь count++ повлияет на важные части внутреннего представления карты, и вы можете получить сбой.

person visitor    schedule 19.10.2010
comment
Хорошо, сделали это и получили такие же результаты, как и вы - ваше объяснение имеет смысл. Следующий вопрос: где он находит исполняемый код? - person slashmais; 19.10.2010
comment
На уровне машины это просто байты и смещения. Машина в основном интерпретирует вызов B::multiply как увеличение целого числа по смещению 0 (поскольку count является первым членом) от указателя this, затем умножает его на значения аргументов функции и возвращает результат. За исключением того, что у вас нет экземпляра B, и используемые им байты на самом деле не являются count членом экземпляра B. - person visitor; 19.10.2010

Ваше приведение в определении макроса HANDLER говорит компилятору: «Заткнись! Я знаю, что делаю!».

Поэтому компилятор затыкается.

У вас все еще есть Undefined Behavior, но одно свойство UB заключается в том, что в некоторых случаях он делает то, что вы наивно ожидаете, или то, что вы хотите.

Но не удивляйтесь, если такой код даст сбой, или вызовет сбои, или загадочный неверный результат в совершенно не связанном коде.

Или, например, заставляет носовых демонов вылетать из вашего носа.

Ура и чт.

person Cheers and hth. - Alf    schedule 19.10.2010
comment
LOL, да, мне интересно, почему использование this-указателя в A успешно вызывает членов в B. "T" проходит странно... - person slashmais; 19.10.2010
comment
Указатели функций должны быть «дикими», но они вызывают правильный код. Ваш ответ не объясняет этого. - person slashmais; 19.10.2010
comment
@slashmais: на практике выполняется правильный код и он возвращает правильный результат, потому что функция имеет ту же сигнатуру, что и typedef. Но это также повредит стек, не сообщая вам об этом. - person Oliver Charlesworth; 19.10.2010
comment
@slashmais: ответ полностью правильный. Ваш код небезопасно приводит функцию-член B к функции-члену A, а затем вызывает ее в экземпляре A. Обработка объекта одного типа как объекта несовместимого типа приводит к неопределенному поведению, которое вообще может делать что угодно. То, что он почти наверняка делает здесь, это портит aA.funcs, что может быть безвредно или может привести к разным видам плохого поведения. Вы должны избегать приведения, если они вам не нужны и вы не можете доказать, что они верны, и избегать приведения в стиле C, как чумы - этот вопрос является хорошим примером того, почему. - person Mike Seymour; 19.10.2010
comment
@Mike Seymour: посетитель дал тот же ответ, теперь остается, где он находит код, который выполняет? может быть, поскольку это такие простые функции, они встроены? - person slashmais; 19.10.2010
comment
@slashmais: мы находимся в сфере неопределенного поведения, поэтому нет гарантированного механизма для поиска кода. Однако указатель функции-члена, вероятно, указывает на ожидаемый код; он просто вызывается с this, указывающим на объект неправильного типа. Но нет никакой гарантии, что другой компилятор сделает то же самое; ваш код недействителен, и его поведение не определено. - person Mike Seymour; 19.10.2010

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

Избавьтесь от своего макроса полностью и не кастуйте. Вероятно, вы можете использовать boost::function и boost::bind, чтобы получить поведение, которое вы действительно хотите.

person CashCow    schedule 19.10.2010

Ваш код вызывает неопределенное поведение, пытаясь вызвать член класса B, используя объект класса A. Мы можем попытаться объяснить, как компилятор может прийти к поведению, которое вы наблюдали, но это не так. гарантировать, что вы получите такое же поведение, если вы что-либо измените (добавите/удалите элемент, измените настройки компилятора или используйте другой компилятор).

Приведением в макросе HANDLER вы говорите компилятору не предупреждать вас об использовании несовместимых типов, а просто делать то, что вы ему говорите. В этом случае вы говорите компилятору переинтерпретировать адрес члена любого класса как адрес члена класса A.

Когда вы позже попытаетесь вызвать, например, 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-таблицу объекта, для которого вызывается функция.

person Bart van Ingen Schenau    schedule 19.10.2010