Обходной путь для указателя-члена на плохой хак?

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

class Variant
{
    void* _value;

    template <typename T1>
    void Variant::ThisCall( T1* pThis )
    {
        typedef void(T1::* fptr)( );
        fptr a;
        int* iiVal = new (&a) int;
        *iiVal = *((int*)_value);
        (pThis->*a)( );
    }
};

// usage
Variant myfunc = &SomeClass::SomeMethod;
SomeClass* s = new SomeClass( );
myfunc.ThisCall( s );

Самое важное, над чем мне приходилось работать в этом решении, это то, что функции указателя на член не могут быть приведены к типу void*. Таким образом, оператор присваивания по существу выполняет обратную операцию. Он берет заданные данные, маскирует их как указатель int (если это сам указатель) и назначает указатель int на void*, что вполне допустимо.

Итак, мой вопрос заключается в следующем: Почему мне кажется, что это ужасное решение проблемы? Мне кажется, что это такой большой взлом, что с ним должны быть серьезные проблемы, но я Я так глубоко погрузился в эту проблему в течение нескольких дней, что не могу заглянуть за нее. Спасибо!

[ИЗМЕНИТЬ №1]

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

class ImplA : public Base
{
public:
    virtual void Print( )
    {
        cout << "ImplA print\n";
    }
};

class ImplB : public Base
{
public:
    virtual void Print( )
    {
        cout << "ImplB print\n";
    }
};

class ImplC : public ImplA
{
public:
    virtual void Print( )
    {
        cout << "ImplC print\n";
    }
};

// usage
Variant x = &Base::Print;
auto b = new ImplA; // replace ImplA with ImplB or ImplC and it works as expected
x.ThisCall( b );

Для получения дополнительной информации я использую VS2010 в качестве компилятора. Спасибо!

[ИЗМЕНИТЬ № 2]

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


person Will Custode    schedule 10.12.2013    source источник
comment
Одна проблема: возможно, не работает с указателем на виртуального члена функция. void* не обязательно должен иметь тот же размер, что и любой указатель функции IIRC.   -  person dyp    schedule 10.12.2013
comment
Указатели функций приведения IIRC недействительны и/или приводят к UB.   -  person RedX    schedule 10.12.2013
comment
@DyP: я только что проверил виртуальную функцию-член, и она работает. Я опубликую код, который я использовал, чтобы доказать это. Но на этом спасибо!   -  person Will Custode    schedule 10.12.2013
comment
Вы пытаетесь обойти то, что было явно запрещено (преобразование указателя функции-члена в указатель объекта). Вот почему вы чувствуете, что это ужасное решение, вероятно.   -  person dyp    schedule 10.12.2013
comment
Это чертовски уродливо, и на то есть веские причины. Вы действительно, в основном, вероятно, не должны этого делать. Это работает во всех компиляторах, которые я встречал, но хорошо продуманное решение с использованием виртуальных методов и интерфейсов, вероятно, лучше, если это возможно в вашем контексте. Иногда при перехвате существующих функций приходится делать какие-то причудливые приведения типов. В зависимости от вашего компилятора/системы могут быть гарантии, которые сделают этот вид безопасным, но он никогда не будет красивым. При написании нового кода используйте интерфейсы и виртуальную диспетчеризацию.   -  person ssube    schedule 10.12.2013
comment
@peechykeen: я абсолютно с тобой согласен. Однако при попытке сделать так, чтобы этот класс Variant поддерживал что-либо, возникло вот что. И теперь я пытаюсь найти лучшее решение, не считая внутреннего синтаксиса, для решения этой проблемы.   -  person Will Custode    schedule 10.12.2013
comment
@WilliamCustode Ваш код содержит неопределенное поведение; то, что он работает для какого-то примера на каком-то компиляторе, не является доказательством того, что он гарантированно работает (для более сложных примеров). Кажется, что размер (виртуального) указателя функции-члена в VS2010 в этом простом примере такой же, как размер void*, однако это неверно, например, для g++. Даже если бы это было так, вы нарушаете правило алиасинга [basic.lval]/10.   -  person dyp    schedule 10.12.2013
comment
Как только вы добавите множественное наследование, минимальный размер указателя на элемент будет равен 8; с виртуальным наследованием для VS2010 будет 12. Даже в этом случае простой пример может дать правильные результаты, если вы не возражаете против гнусавых демонов.   -  person dyp    schedule 10.12.2013


Ответы (2)


Игнорирование нарушения псевдонимов, которое само по себе делает ваш код незаконным, то, что вы делаете, эквивалентно этому:

    typedef void(T1::* fptr)( );
    fptr a;
    memcpy(&a, _value, sizeof(int));
    (pThis->*a)( );

Должно быть очевидно, почему это непереносимо; нет гарантии, что fptr имеет тот же размер, что и int, поэтому вы, скорее всего, либо частично инициализируете его хранилище, либо переполните его.

Это будет допустимо, если вы замените sizeof(int) на sizeof(fptr) и убедитесь, что хранилище, на которое указывает _value, достаточно велико, чтобы вместить fptr. Однако вы все равно должны использовать memcpy вместо алиасинга; memcpy гарантированно работает (3.9p2), в то время как псевдонимы могут привести к трудно обнаруживаемым ошибкам, которые обычно проявляются или изменяют поведение при оптимизации.

person ecatmur    schedule 24.01.2014
comment
Мне нравится твой ответ. Единственный человек, который действительно обратил внимание на вопрос. Спасибо. - person Will Custode; 24.01.2014

Как упоминалось в комментариях, этот код не очень переносим и безопасен. Если вы просто храните указатели на функции, я бы предложил использовать обертки std::function или boost::function:

template <typename T>
class Variant {
    std::function<void(T*)> fun;
public:
    Variant(void (T:: *ptr)() ) : fun(ptr) {
    }

    void ThisCall(T* pThis) {
        fun(pThis);
    }
};

Variant<SomeClass> myfunc = &SomeClass::SomeMethod;
SomeClass* s = new SomeClass( );
myfunc.ThisCall( s );

Но если вы действительно хотите что-то хранить, почему бы просто не использовать boost::any library?

class VariantWithAny {
    boost::any val;
public:
    VariantWithAny() {}

    VariantWithAny(const boost::any& val) : val(val) {}

    VariantWithAny& operator=(const boost::any& val) {
        this->val = val;
        return *this;
    }

    template <typename T>
    void ThisCall(T* pThis) {
        typedef void (T::* fptr)();
        fptr a = boost::any_cast<fptr>(val);
        (pThis->*a)( );
    }
};

VariantWithAny myfunc2(&SomeClass::SomeMethod1);
myfunc2 = &SomeClass::SomeMethod2;
SomeClass* s2 = new SomeClass( );
myfunc2.ThisCall( s2 );

boost::any_cast безопасен и выдаст исключение (boost::bad_any_cast), если типы не совпадают.

[EDIT]. Трюк, который использует boost::any, заключается в том, чтобы сохранить значение в классе держателя шаблона, который наследуется от чисто виртуального заполнителя. Вот почему он может содержать практически любое значение и не должен приводить его к (void*). Проверьте реализацию - это очень маленький файл.

person Maciek B    schedule 22.01.2014
comment
Зачем писать собственный вариант, если он просто обертывает существующую реализацию? - person Will Custode; 23.01.2014
comment
Это просто обертка, соответствующая интерфейсу, который вы используете. Если вам не нужен метод ThisCall, вы можете использовать boost::any напрямую - person Maciek B; 24.01.2014