Как добиться динамического полиморфизма (диспетчеризация вызовов во время выполнения) по несвязанным типам? - программирование
Подтвердить что ты не робот

Как добиться динамического полиморфизма (диспетчеризация вызовов во время выполнения) по несвязанным типам?

ЗАДАЧА:

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

ОПРЕДЕЛЕНИЕ ПРОБЛЕМЫ:

Учитывая следующее:

  • два или более несвязанных типа A1, ..., An, каждый из которых имеет метод с именем f, возможно с разными сигнатурами, но с тем же типом возврата R; и
  • a boost::variant<A1*, ..., An*> объект v (или любой другой тип варианта), который может и должен принимать в любое время одно значение любого из этих типов;

Моя цель - написать инструкции, концептуально эквивалентные v.f(arg_1, ..., arg_m);, которые будут иметь отправлено во время выполнения для функции Ai::f, если фактический тип значения, содержащегося в v, равен Ai. Если аргументы вызова несовместимы с формальными параметрами каждой функции Ai, компилятор должен вызвать ошибку.

Конечно, мне не нужно придерживаться синтаксиса v.f(arg_1, ..., arg_m): например, что-то вроде call(v, f, ...) также приемлемо.

Я пытался добиться этого на С++, но до сих пор мне не удалось найти решение хорошего (у меня есть куча плохих). Ниже я уточняю, что я имею в виду под "хорошим решением".

ТРУДНОСТИ:

A хорошее решение - это все, что позволяет мне подражать идиоме v.f(...), например. call_on_variant(v, f, ...); и удовлетворяет следующим ограничениям:

  • не требует какой-либо отдельной декларации для каждой функции f, которая должна быть вызвана таким образом (например, ENABLE_CALL_ON_VARIANT(f)) или для любого списка несвязанных типов A1, ..., An, которые могут быть обработаны полиморфно (например, ENABLE_VARIANT_CALL(A1, ..., An)) где-то еще в коде, особенно в глобальной области;
  • не требует, чтобы явно указывал типы входных аргументов при выполнении вызова (например, call_on_variant<int, double, string>(v, f, ...)). Именование типа возврата в порядке, поэтому допустимо call_on_variant<void>(v, f, ...).

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

Пример:

struct A1 { void f(int, double, string) { cout << "A"; } };
struct A2 { void f(int, double, string) { cout << "B"; } };
struct A3 { void f(int, double, string) { cout << "C"; } };

using V = boost::variant<A1, A2, A3>;

// Do not want anything like the following here:
// ENABLE_VARIANT_CALL(foo, <whatever>)

int main()
{
    A a;
    B b;
    C c;

    V v = &a;
    call_on_variant(v, f, 42, 3.14, "hello");

    // Do not want anything like the following here:
    // call_on_variant<int, double, string>(v, f, 42, 3.14, "hello");

    V v = &b;
    call_on_variant(v, f, 42, 3.14, "hello");

    V v = &c;
    call_on_variant(v, f, 42, 3.14, "hello");
}

Выход этой программы должен быть: ABC.

ЛУЧШЕЕ (НЕИСПРАВНО) ПОПЫТКА:

Самое близкое, что я получил к желаемому решению, - это макрос:

#define call_on_variant(R, v, f, ...) \
[&] () -> R { \
    struct caller : public boost::static_visitor<void> \
    { \
        template<typename T> \
        R operator () (T* pObj) \
        { \
            pObj->f(__VA_ARGS__); \
        } \
    }; \
    caller c; \
    return v.apply_visitor(c); \
}();

Что будет работать отлично, , если в локальных классах допустимы только члены шаблона (см. этот вопрос). У кого-нибудь есть идея, как это исправить, или предложить альтернативный подход?

4b9b3361

Ответ 1

Некоторое время прошло, С++ 14 завершается, а компиляторы добавляют поддержку для новых функций, таких как общие лямбды.

Общие лямбды вместе с машинами, показанными ниже, позволяют достичь желаемого (динамического) полиморфизма с несвязанными классами:

#include <boost/variant.hpp>

template<typename R, typename F>
class delegating_visitor : public boost::static_visitor<R>
{
public:
    delegating_visitor(F&& f) : _f(std::forward<F>(f)) { }
    template<typename T>
    R operator () (T x) { return _f(x); }
private:
    F _f;
};

template<typename R, typename F>
auto make_visitor(F&& f)
{
    using visitor_type = delegating_visitor<R, std::remove_reference_t<F>>;
    return visitor_type(std::forward<F>(f));
}

template<typename R, typename V, typename F>
auto vcall(V&& vt, F&& f)
{
    auto v = make_visitor<R>(std::forward<F>(f));
    return vt.apply_visitor(v);
}

#define call_on_variant(val, fxn_expr) \
    vcall<int>(val, [] (auto x) { return x-> ## fxn_expr; });

Допустим это на практике. Предположим, что существуют следующие два несвязанных класса:

#include <iostream>
#include <string>

struct A
{
    int foo(int i, double d, std::string s) const
    { 
        std::cout << "A::foo(" << i << ", " << d << ", " << s << ")"; 
        return 1; 
    }
};

struct B
{
    int foo(int i, double d, std::string s) const
    { 
        std::cout << "B::foo(" << i << ", " << d << ", " << s << ")"; 
        return 2;
    }
};

Полиморфно вызывать foo() таким образом:

int main()
{
    A a;
    B b;

    boost::variant<A*, B*> v = &a;
    auto res1 = call_on_variant(v, foo(42, 3.14, "Hello"));
    std::cout << std::endl<< res1 << std::endl;

    v = &b;
    auto res2 = call_on_variant(v, foo(1337, 6.28, "World"));
    std::cout << std::endl<< res2 << std::endl;
}

И результат, как и ожидалось:

A::foo(42, 3.14, Hello)
1
B::foo(1337, 6.28, World)
2

Программа была протестирована на VC12 с CTP в ноябре 2013 года. К сожалению, я не знаю ни одного онлайн-компилятора, который поддерживает общие лямбды, поэтому я не могу опубликовать живой пример.

Ответ 2

ОК, вот дикий снимок:

template <typename R, typename ...Args>
struct visitor : boost::static_visitor<R>
{
    template <typename T>
    R operator()(T & x)
    { 
        return tuple_unpack(x, t);   // this needs a bit of code
    }

    visitor(Args const &... args) : t(args...) { }

private:
    std::tuple<Args...> t;
};

template <typename R, typename Var, typename ...Args>
R call_on_variant(Var & var, Args const &... args)
{
    return boost::apply_visitor(visitor<R, Args...>(args...), var);
}

Использование:

R result = call_on_variant<R>(my_var, 12, "Hello", true);

Я скрыл определенную работу, необходимую для вызова функции, распаковывая кортеж, но я считаю, что это было сделано в другом месте на SO.

Кроме того, если вам нужно хранить ссылки, а не копии аргументов, это может быть сделано, но требует большей осторожности. (У вас может быть кортеж ссылок. Но вам нужно подумать, хотите ли вы также разрешить временные объекты.)

Ответ 3

К сожалению, это невозможно сделать на С++ (пока - см. выводы). Выполняет доказательство.

СООБРАЖЕНИЕ 1: [о необходимости шаблонов]

Чтобы определить правильную функцию-член Ai::f для вызова во время выполнения, когда выполняется выражение call_on_variant(v, f, ...) (или любая его эквивалентная форма), необходимо, учитывая вариантный объект v, для получения типа Ai значения, которое удерживается v. Для этого обязательно требуется определение хотя бы одного шаблона (класса или функции).

Причиной этого является то, что независимо от того, как это делается, требуется итерация по всем типам, которые может содержать вариант (список типов отображается как boost::variant<...>::types, проверьте, вариант имеет значение этого типа (через boost::get<>) и (если это так) извлекает это значение в качестве указателя, через который должен выполняться вызов функции-члена (внутренне это также означает boost::apply_visitor<>).

Для каждого отдельного типа в списке это можно сделать следующим образом:

using types = boost::variant<A1*, ..., An*>::types;
mpl::at_c<types, I>::type* ppObj = (get<mpl::at_c<types, I>::type>(&var));
if (ppObj != NULL)
{
    (*ppObj)->f(...);
}

Где I - константа времени компиляции. К сожалению, С++ не поддерживает static for idiom, который позволил бы генерировать последовательность таких фрагментов компилятором на основе время компиляции для цикла. Вместо этого следует использовать методы мета-программирования шаблона, например:

mpl::for_each<types>(F());

где F - функтор с оператором шаблона вызова. Прямо или косвенно, должен быть определен хотя бы один шаблон класса или функции, поскольку отсутствие static for заставляет программиста кодировать процедуру, которая должна быть повторена для каждого типа в целом.

РАССМОТРЕНИЕ 2: [о необходимости локальности]

Одно из ограничений для требуемого решения (требование 1 раздела "ОГРАНИЧЕНИЯ" в тексте вопроса) состоит в том, что нет необходимости добавлять глобальные декларации или любое другое объявление в любой другой области, кроме той, где функция вызов выполняется. Поэтому, независимо от того, участвует ли макрообъем или метапрограммирование шаблона, необходимо выполнить в том месте, где происходит вызов функции.

Это проблематично, потому что вышеизложенное "СООТВЕТСТВИЕ 1" доказало, что для выполнения задачи необходимо определить хотя бы один шаблон. Проблема в том, что С++ не позволяет определять шаблоны в локальной области. Это справедливо для шаблонов классов и шаблонов функций, и нет способа преодолеть это ограничение. В §14/2:

"Объявление шаблона может отображаться только как область пространства имен или объявление области видимости класса

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

РАССМОТРЕНИЕ 3: [по именам функций]

Так как макрос call_on_variant() (или любая эквивалентная конструкция) должен иметь возможность обрабатывать любую возможную функцию F, имя F должно быть передано в качестве аргумента в наш механизм разрешения на основе шаблонов. Важно подчеркнуть, что должно быть передано только имя функции, так как конкретная функция Ai::f, которая должна быть вызвана, должна определяться машиной шаблонов.

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

ВЫВОД:

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

БУДУЩИЕ ВОЗМОЖНОСТИ:

Общепринятые лямбды, которые сильно вынуждены войти в следующий стандарт С++, на самом деле являются локальными классами с оператором шаблона.

Таким образом, хотя макрос, который я разместил в конце текста вопроса, по-прежнему не работает, альтернативный подход кажется жизнеспособным (с некоторой настройкой, необходимой для обработки возвращаемых типов):

// Helper template for type resolution
template<typename F, typename V>
struct extractor
{
    extractor(F f, V& v) : _f(f), _v(v) { }

    template<typename T>
    void operator () (T pObj)
    {
        T* ppObj = get<T>(&_v));
        if (ppObj != NULL)
        {
            _f(*ppObj);
            return;
        }
    }

    F _f;
    V& _v;
};

// v is an object of type boost::variant<A1*, ..., An*>;
// f is the name of the function to be invoked;
// The remaining arguments are the call arguments.
#define call_on_variant(v, f, ...) \
    using types = decltype(v)::types; \
    auto lam = [&] (auto pObj) \
    { \
        (*pObj)->f(__VA_ARGS__); \
    }; \
    extractor<decltype(lam), decltype(v)>(); \
    mpl::for_each<types>(ex);

ЗАКЛЮЧИТЕЛЬНЫЕ ЗАМЕЧАНИЯ:

Это интересный случай типа безопасного вызова, который (к сожалению) не поддерживается С++. Настоящая статья Маца Маркуса, Яакко Джарви и Шона Родитель, похоже, показывает, что динамический полиморфизм на несвязанных типах имеет решающее значение для достижения важного (в моем мнение, фундаментальное и неизбежное) сдвиг парадигмы в программировании.

Ответ 4

Я как-то решил это, моделируя делегатов .NET:

template<typename T>
class Delegate
{
    //static_assert(false, "T must be a function type");
};

template<typename ReturnType>
class Delegate<ReturnType()>
{
private:
    class HelperBase
    {
    public:
        HelperBase()
        {
        }

        virtual ~HelperBase()
        {
        }

        virtual ReturnType operator()() const = 0;
        virtual bool operator==(const HelperBase& hb) const = 0;
        virtual HelperBase* Clone() const = 0;
    };

    template<typename Class>
    class Helper : public HelperBase
    {
    private:
        Class* m_pObject;
        ReturnType(Class::*m_pMethod)();

    public:
        Helper(Class* pObject, ReturnType(Class::*pMethod)()) : m_pObject(pObject), m_pMethod(pMethod)
        {
        }

        virtual ~Helper()
        {
        }

        virtual ReturnType operator()() const
        {
            return (m_pObject->*m_pMethod)();
        }

        virtual bool operator==(const HelperBase& hb) const
        {
            const Helper& h = static_cast<const Helper&>(hb);
            return m_pObject == h.m_pObject && m_pMethod == h.m_pMethod;
        }

        virtual HelperBase* Clone() const
        {
            return new Helper(*this);
        }
    };

    HelperBase* m_pHelperBase;

public:
    template<typename Class>
    Delegate(Class* pObject, ReturnType(Class::*pMethod)())
    {
        m_pHelperBase = new Helper<Class>(pObject, pMethod);
    }

    Delegate(const Delegate& d)
    {
        m_pHelperBase = d.m_pHelperBase->Clone();
    }

    Delegate(Delegate&& d)
    {
        m_pHelperBase = d.m_pHelperBase;
        d.m_pHelperBase = nullptr;
    }

    ~Delegate()
    {
        delete m_pHelperBase;
    }

    Delegate& operator=(const Delegate& d)
    {
        if (this != &d)
        {
            delete m_pHelperBase;
            m_pHelperBase = d.m_pHelperBase->Clone();
        }

        return *this;
    }

    Delegate& operator=(Delegate&& d)
    {
        if (this != &d)
        {
            delete m_pHelperBase;
            m_pHelperBase = d.m_pHelperBase;
            d.m_pHelperBase = nullptr;
        }

        return *this;
    }

    ReturnType operator()() const
    {
        (*m_pHelperBase)();
    }

    bool operator==(const Delegate& d) const
    {
        return *m_pHelperBase == *d.m_pHelperBase;
    }

    bool operator!=(const Delegate& d) const
    {
        return !(*this == d);
    }
};

Вы можете использовать его так же, как .NET delegates:

class A
{
public:
    void M() { ... }
};

class B
{
public:
    void M() { ... }
};

A a;
B b;

Delegate<void()> d = Delegate<void()>(&a, &A::M);
d(); // calls A::M

d = Delegate<void()>(&b, &B::M);
d(); // calls B::M

Это работает с методами, у которых нет аргументов. Если вы можете использовать С++ 11, вы можете изменить его, чтобы использовать вариативные шаблоны для обработки любого количества параметров. Без С++ 11 вам нужно добавить дополнительные специализации делегата для обработки определенного количества параметров:

template<typename ReturnType, typename Arg1>
class Delegate<ReturnType(Arg1)>
{
    ...
};

template<typename ReturnType, typename Arg1, typename Arg2>
class Delegate<ReturnType(Arg1, Arg2)>
{
    ...
};

С помощью этого класса Delegate вы также можете эмулировать события .NET, которые основаны на делегатах.