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

ЦЕЛЬ:

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

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

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

  • два или более несвязанный типы A1, ..., Anкаждый из которых имеет метод с именем fвозможно с разными подписями, но с тем же типом возврата R; а также
  • 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, ...) также приемлемо.

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

ТРУДНОСТИ:

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

  1. не требует какого-либо отдельная декларация для каждой функции f это должно быть названо так (например, ENABLE_CALL_ON_VARIANT(f)) или для любого списка несвязанных типов A1, ..., An которые можно лечить полиморфно (например, ENABLE_VARIANT_CALL(A1, ..., An)) где-то еще в коде, особенно в глобальной области видимости;
  2. не требует явно указать типы входных аргументов во время звонка (например, 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); \
}();

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

14

Решение

Прошло немного времени, C ++ 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 года. К сожалению, я не знаю ни одного онлайн-компилятора, который бы поддерживал общие лямбда-выражения, поэтому я не могу опубликовать живой пример.

7

Другие решения

Хорошо, вот дикий выстрел:

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.

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

4

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

РАССМОТРЕНИЕ 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 это время компиляции постоянная. К сожалению, C ++ не учесть static for идиома это позволило бы последовательности таких фрагментов генерироваться компилятором на основе время компиляции для цикла. Вместо, шаблонное метапрограммирование методы должны быть использованы, такие как:

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

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

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

Одно из ограничений для желаемого решения (требование 1 раздела «ТРУДНОСТИ«в тексте вопроса) заключается в том, что нет необходимости добавлять глобальные объявления или любые другие объявления в любой другой области, кроме той, в которой выполняется вызов функции. Следовательно, независимо от того, задействовано ли расширение макроса или шаблонное метапрограммирование , Что должно быть сделано должно быть сделано в том месте, где происходит вызов функции.

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

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

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

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

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

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

ЗАКЛЮЧЕНИЕ:

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

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

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

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

// 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);

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

Это интересный случай безопасного типа вызова, который (к сожалению) не поддерживается C ++. Эта бумага Mat Marcus, Jaakko Jarvi и Sean Parent, кажется, показывают, что динамический полиморфизм на несвязанных типах имеет решающее значение для достижения важного (на мой взгляд, фундаментального и неизбежного) смена парадигмы в программировании.

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:

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

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

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

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

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

0
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector