Привет!
Не знал, как поточнее назвать статью, но хотелось бы разобрать одну маленькую задачку, которая звучит следующим образом:
На вход подаётся отформатированная некоторым образом строка, в которой указаны имя функции, её аргументы и типы аргументов. Нужно иметь возможность вызвать соответствующий обработчик функции, корректно передав все аргументы.
Например, так ActionScript пытается вызвать функцию test с тремя аргументами str, false, 1.0(соответственно типы аргументов: String, Boolean, Number):
<invoke name="test" returntype="xml"><arguments><string>str</string><false/><number>1.0</number></arguments></invoke>
Хотелось бы, чтобы со стороны C++ была вызвана соответствующая функция:
void test_handler(const std::wstring& str, bool flag, double n);
Под катом — реализация с использованием нового стандарта и, для сравнения, реализация с использованием старого стандарта(и капельки boost-а).
Приступим. Для начала нужно как-то разобрать строку. Поскольку это не суть задачи, то, для разбора xml, будем использовать Boost.PropertyTree. Всё это спрячем в спомогательный класс InvokeParser
, который будет хранить имя функции и массив пар тип аргумента-его значение:
#include "boost/property_tree/ptree.hpp"
#include "boost/property_tree/xml_parser.hpp"
namespace as3 {
class InvokeParser
{
public:
using ArgsContainer = std::vector<
std::pair<
std::wstring, // Argument type
std::wstring // Argument value
>>;
public:
InvokeParser()
: invoke_name_()
, arguments_()
{
}
bool parse(const std::wstring& str)
{
using namespace boost;
using namespace boost::property_tree;
try
{
std::wistringstream stream(str);
wptree xml;
read_xml(stream, xml);
// Are 'invoke' tag attributes and 'arguments' tag exists?
auto invoke_attribs = xml.get_child_optional(L"invoke.<xmlattr>");
auto arguments_xml = xml.get_child_optional(L"invoke.arguments");
if(!invoke_attribs || !arguments_xml)
return false;
// Is 'name' exists ?
auto name = invoke_attribs->get_optional<std::wstring>(L"name");
if(!name)
return false;
invoke_name_ = *name;
arguments_.reserve(arguments_xml->size());
for(const auto& arg_value_pair : *arguments_xml)
{
std::wstring arg_type = arg_value_pair.first;
std::wstring arg_value = arg_value_pair.second.get_value(L"");
if((arg_type == L"true") || (arg_type == L"false"))
{
arg_value = arg_type;
arg_type = L"bool";
}
arguments_.emplace_back(arg_type, arg_value);
}
return true;
}
catch(const boost::property_tree::xml_parser_error& /*parse_exc*/)
{
}
catch(...)
{
}
return false;
}
std::wstring function_name() const
{
return invoke_name_;
}
size_t arguments_count() const
{
return arguments_.size();
}
const ArgsContainer& arguments() const
{
return arguments_;
}
private:
std::wstring invoke_name_;
ArgsContainer arguments_;
};
} // as3
Теперь напишем шаблонный класс Type
, который будет иметь один параметр — некий C++-тип, в который нужно будет превратить строку, а также узнать соответствующее имя со стороны ActionScript. Например:
Type<short>::convert(L"20"); // Вернёт 20, тип short
Type<short>::name(); // short для ActionScript это "number"
Код шаблонного класса Type
:
template<typename CppType>
struct Type :
std::enable_if<
!std::is_array<CppType>::value,
TypeHelper<
typename std::decay<CppType>::type>
>::type
{
};
template<typename CppType>
struct Type<CppType*>
{
};
Здесь есть минимальный предосторожности для неосторожного пользователя данного шаблона, например, нельзя инстанцировать данный шаблон указателем на какой-то тип, так как мы не используем указатели(конкретно для ActionScript — указателей попросту нет). Конечно здесь не все предострожности, но их можно легко добавить.
Как видно, основную роботу выполняет другой шаблонный класс TypeHelper
. TypeHelper
инстанцируется «голым» типом. Например, для const std::wstring&
получится std::wstring
и т.д. Это делается с помощью decay. И делается это для того, чтобы убрать различия между функциями который имеют сигнатуры типа f(const std::wstring&)
и f(std::wstring)
. Делаем также некоторую предосторожность для массивов, так как в этом случае послушный decay
прекрасно справиться с роботой и мы получим не совсем то, что хотели.
Основной шаблон TypeHelper
. Он будет служить для преобразования чисел. В нашем случае он будет прекрасно работать для Арифметических типов, хотя, по-хорошему, нужно-бы исключить все символьные типы(это сделать не сложно немножко модифицировав is_valid_type
с помощью std::is_same
).
template<typename CppType>
struct TypeHelper
{
private:
enum { is_valid_type = std::is_arithmetic<CppType>::value };
public:
typedef typename std::enable_if<is_valid_type, CppType>::type Type;
static typename std::enable_if<is_valid_type, std::wstring>::type name()
{
return L"number";
}
// Convert AS3 number type from string to @CppType
static Type convert(const std::wstring& str)
{
double value = std::stod(str);
return static_cast<Type>(value);
}
};
Как видно, имя всех числовых типов, в случае ActionScript — number. Для преобразования со строки, сначала аккуратно преобразовываем в double
, а потом в нужный нам тип.
Также нам нужна другая обработка для типов bool
, std::wstring
и void
:
template<>
struct TypeHelper<bool>
{
typedef bool Type;
static std::wstring name()
{
return L"bool";
}
static bool convert(const std::wstring& str)
{
return (str == L"true");
}
};
template<>
struct TypeHelper<std::wstring>
{
typedef std::wstring Type;
static std::wstring name()
{
return L"string";
}
static std::wstring convert(const std::wstring& str)
{
return str;
}
};
template<>
struct TypeHelper<void>
{
typedef void Type;
static std::wstring name()
{
return L"undefined";
}
static void convert(const std::wstring& /*str*/)
{
}
};
Вот и всё, по-сути! Осталось аккуратно соединить всё вместе. Поскольку нужно иметь удобный механизм создания обработчиков соответствующих функций(хранение всех обработчиков в контейнере и т.д.), создадим интерфейс IFunction
:
struct IFunction
{
virtual bool call(const InvokeParser& parser) = 0;
virtual ~IFunction()
{
}
};
А вот классы-наследники уже будут знать, какую функцию нужно вызвать для конкретного случая. Снова шаблон:
template<typename ReturnType, typename... Args>
struct Function :
public IFunction
{
Function(const std::wstring& function_name, ReturnType (*f)(Args...))
: f_(f)
, name_(function_name)
{
}
bool call(const InvokeParser& parser)
{
if(name_ != parser.function_name())
return false;
const auto ArgsCount = sizeof...(Args);
if(ArgsCount != parser.arguments_count())
return false;
auto indexes = typename generate_sequence<ArgsCount>::type();
auto args = parser.arguments();
if(!validate_types(args, indexes))
return false;
return call(args, indexes);
}
private:
template<int... S>
bool validate_types(const InvokeParser::ArgsContainer& args, sequence<S...>)
{
std::array<std::wstring, sizeof...(Args)> cpp_types = { Type<Args>::name()... };
std::array<std::wstring, sizeof...(S)> as3_types = { args[S].first... };
return (cpp_types == as3_types);
}
template<int... S>
bool call(const InvokeParser::ArgsContainer& args, sequence<S...>)
{
f_(Type<Args>::convert(args[S].second)...);
return true;
}
protected:
std::function<ReturnType (Args...)> f_;
std::wstring name_;
};
template<typename ReturnType, typename... Args>
std::shared_ptr<IFunction> make_function(const std::wstring& as3_function_name, ReturnType (*f)(Args...))
{
return std::make_shared<Function<ReturnType, Args...>>(as3_function_name, f);
}
Сначала покажу как использовать:
void test_handler(const std::wstring& str, bool flag, double n)
{
std::wcout << L"test: " << str << L", " << std::boolalpha << flag << ", " << n << std::endl;
}
int main()
{
as3::InvokeParser parser;
std::wstring str = L"<invoke name="test" returntype="xml">"
L"<arguments><string>str</string><false/><number>1.0</number></arguments>"
L"</invoke>";
if(parser.parse(str))
{
auto function = as3::make_function(L"test", test_handler);
function->call(parser);
}
}
Перейдём к деталям. Во-первых, есть вспомогательная функция as3::make_function()
, которая помогает не думать о типе передаваемого callback-а.
Во-вторых, для того, чтобы пройтись(более правильно — распаковать «паттерн», смотрим ссылку Parameter pack(ниже)) по пакету параметров(как перевести? Parameter pack) используются вспомогательные структуры sequence
и generate_sequence
:
template<size_t N, size_t... Sequence>
struct generate_sequence :
generate_sequence<N - 1, N - 1, Sequence...>
{
};
template<size_t...>
struct sequence
{
};
template<int... Sequence>
struct generate_sequence<0, Sequence...>
{
typedef sequence<Sequence...> type;
};
Подсмотрено на stackoverflow.
В двух словах generate_sequence<N>
генерирует пакет параметров 0, 1, 2, ... N - 1
, который сохраняется(читать: «выводится компилятором») с помощью sequence
. Это всё нужно для Pack expansion(буду переводить как «распаковка пакета»).
Например:
У нас есть пакет параметров typename... Args
и функция f
. Мы хотим вызвать f
, передав каждое значение пакета некоторой функции, а результат этой функции уже передать f
:
template<typename... Args>
struct Test
{
template<int... S>
static bool test(const std::vector<pair<int, float>>& args, sequence<S...>)
{
f_(Type<Args>::convert(args[S].second)...);
return true;
}
};
// где-то в коде test(args, typename generate_sequence<sizeof...(Args)>::type())
Вызов test
для Args = <int, float>
превратится в вызов:
f_(Type<int>::convert(args[0].second), Type<float>::convert(args[1].second))
Вот и вся магия!
Наша функция-член Function::validate_types
создаёт два массива. Один массив содержит имена C++-типов в ActionScript-е, а другой — имена типов, со входящей строки. Если массивы не одинаковы — у нас некорректная сигнатура функции! И мы можем это диагностировать. Вот что значит мощь шаблонов!
А вспомогательная функция-член call(const InvokeParser::ArgsContainer& args, sequence<S...>)
делает то, что в примере выше, только для нашего случая.
Всё — с помощью make_function()
можно делать регистрацию обработчиков, поскольку мы имеем полиморфный тип IFunction
, обработчики можно спокойно сохранять в массиве(да в чём угодно) и при поступлении очередной строки вызывать соответствующий обработчик.
Итак, а теперь код для старого стандарта, для максимального количества аргументов равного четырём с использованием boost::function_traits
и совсем немножко mpl
:)
class InvokeParser
{
public:
typedef std::vector<std::pair<std::wstring, std::wstring> > ArgsContainer;
typedef ArgsContainer::value_type TypeValuePair;
public:
InvokeParser()
: invoke_name_()
, arguments_()
{
}
bool parse(const std::wstring& str)
{
using namespace boost;
using namespace boost::property_tree;
try
{
std::wistringstream stream(str);
wptree xml;
read_xml(stream, xml);
optional<wptree&> invoke_attribs = xml.get_child_optional(L"invoke.<xmlattr>");
optional<wptree&> arguments_xml = xml.get_child_optional(L"invoke.arguments");
if(!invoke_attribs || !arguments_xml)
return false;
optional<std::wstring> name = invoke_attribs->get_optional<std::wstring>(L"name");
if(!name)
return false;
invoke_name_ = *name;
arguments_.reserve(arguments_xml->size());
for(wptree::const_iterator arg_value_pair = arguments_xml->begin(), end = arguments_xml->end(); arg_value_pair != end; ++arg_value_pair)
{
std::wstring arg_type = arg_value_pair->first;
std::wstring arg_value = arg_value_pair->second.get_value(L"");
if((arg_type == L"true") || (arg_type == L"false"))
{
arg_value = arg_type;
arg_type = L"bool";
}
arguments_.push_back(TypeValuePair(arg_type, arg_value));
}
return true;
}
catch(const boost::property_tree::xml_parser_error& /*parse_exc*/)
{
}
catch(...)
{
}
return false;
}
std::wstring function_name() const
{
return invoke_name_;
}
size_t arguments_count() const
{
return arguments_.size();
}
const ArgsContainer& arguments() const
{
return arguments_;
}
private:
std::wstring invoke_name_;
ArgsContainer arguments_;
};
template<typename CppType>
struct TypeHelper
{
private:
// Arithmetic types are http://en.cppreference.com/w/cpp/language/types.
// Need to exclude 'Character types' from this list
// (For 'Boolean type' this template has full specialization)
typedef boost::mpl::and_<
boost::is_arithmetic<CppType>,
boost::mpl::not_<boost::is_same<CppType, char> >,
boost::mpl::not_<boost::is_same<CppType, wchar_t> >,
boost::mpl::not_<boost::is_same<CppType, unsigned char> >,
boost::mpl::not_<boost::is_same<CppType, signed char> > > ValidCppType;
public:
// We can get C++ type name equivalent for AS3 "number" type only if
// C++ type @CppType is @ValidCppType(see above)
typedef typename boost::enable_if<
ValidCppType,
CppType>::type Type;
// Get AS3 type name for given @CppType(see @ValidCppType)
static
typename boost::enable_if<
ValidCppType,
std::wstring>::type name()
{
return L"number";
}
// Convert AS3 number type from string to @CppType(see @ValidCppType)
static
Type convert(const std::wstring& str)
{
double value = from_string<wchar_t, double>(str);
// TODO: Use boost type cast
return static_cast<Type>(value);
}
};
template<>
struct TypeHelper<bool>
{
typedef bool Type;
// AS3 type name for boolean type
static std::wstring name()
{
return L"bool";
}
// Convert AS3 boolean value from string to our bool
static bool convert(const std::wstring& str)
{
return (str == L"true");
}
};
template<>
struct TypeHelper<std::wstring>
{
typedef std::wstring Type;
static std::wstring name()
{
return L"string";
}
static std::wstring convert(const std::wstring& str)
{
// Ok, do nothing
return str;
}
};
template<>
struct TypeHelper<void>
{
typedef void Type;
// AS3 type name for void type..
static std::wstring name()
{
return L"undefined";
}
static void convert(const std::wstring& /*str*/)
{
// Oops..
ASSERT_MESSAGE(false, "Can't convert from sring to void");
}
};
// @TypeHelper provides implementation
// only for "number" type(arithmetic, without characters type), bool, string and void.
// For any other type @TypeHelper will be empty.
// decay is used for removing cv-qualifier. But it's not what we want for arrays.
// That's why using enable_if
template<typename CppType>
struct FlashType :
boost::enable_if<
boost::mpl::not_<
boost::is_array<CppType> >,
TypeHelper<
typename std::tr1::decay<CppType>::type>
>::type
{
};
// Partial specialization for pointers
// There is no conversion from AS3 type to C++ pointer..
template<typename CppType>
struct FlashType<CppType*>
{
// static assert
};
То, чего не было ранее — FunctionCaller
— заменяет распаковку....:
template<int N>
struct FunctionCaller
{
template<typename Function>
static bool call(Function /*f*/, const InvokeParser::ArgsContainer& /*args*/
#if defined(DEBUG)
, const std::wstring& /*dbg_function_name*/
#endif
)
{
ASSERT_MESSAGE_AND_RETURN_VALUE(
false,
"Provide full FunctionCaller specialization for given arguments count",
false);
}
};
template<>
struct FunctionCaller<0>
{
template<typename Function>
static bool call(Function f, const InvokeParser::ArgsContainer& /*args*/
#if defined(DEBUG)
, const std::wstring& /*dbg_function_name*/
#endif
)
{
// Call function without args
f();
return true;
}
};
template<>
struct FunctionCaller<1>
{
template<typename Function>
static bool call(Function f, const InvokeParser::ArgsContainer& args
#if defined(DEBUG)
, const std::wstring& dbg_function_name
#endif
)
{
typedef FlashType<typename boost::function_traits<Function>::arg1_type> Arg1;
const InvokeParser::TypeValuePair& arg = args[0];
if(Arg1::name() != arg.first)
{
#if defined(DEBUG)
::OutputDebugStringW(Sprintf<wchar_t>(
L"Function: "%s":n"
L"%s -> %sn",
dbg_function_name.c_str(),
Arg1::name().c_str(), arg.first.c_str()).c_str());
#endif
ASSERT_MESSAGE_AND_RETURN_VALUE(false, "Type mismatch", false);
}
// Call function with 1 arg
f(Arg1::convert(arg.second));
return true;
}
};
template<>
struct FunctionCaller<2>
{
template<typename Function>
static bool call(Function f, const InvokeParser::ArgsContainer& args
#if defined(DEBUG)
, const std::wstring& dbg_function_name
#endif
)
{
typedef FlashType<typename boost::function_traits<Function>::arg1_type> Arg1;
typedef FlashType<typename boost::function_traits<Function>::arg2_type> Arg2;
const InvokeParser::TypeValuePair& arg1 = args[0];
const InvokeParser::TypeValuePair& arg2 = args[1];
if((Arg1::name() != arg1.first) ||
(Arg2::name() != arg2.first))
{
#if defined(DEBUG)
::OutputDebugStringW(Sprintf<wchar_t>(
L"Function: "%s":n"
L"%s -> %sn"
L"%s -> %sn",
dbg_function_name.c_str(),
Arg1::name().c_str(), arg1.first.c_str(),
Arg2::name().c_str(), arg2.first.c_str()).c_str());
#endif
ASSERT_MESSAGE_AND_RETURN_VALUE(false, "Type mismatch", false);
}
// Call function with 2 args
f(Arg1::convert(arg1.second),
Arg2::convert(arg2.second));
return true;
}
};
template<>
struct FunctionCaller<3>
{
template<typename Function>
static bool call(Function f, const InvokeParser::ArgsContainer& args
#if defined(DEBUG)
, const std::wstring& dbg_function_name
#endif
)
{
typedef FlashType<typename boost::function_traits<Function>::arg1_type> Arg1;
typedef FlashType<typename boost::function_traits<Function>::arg2_type> Arg2;
typedef FlashType<typename boost::function_traits<Function>::arg3_type> Arg3;
const InvokeParser::TypeValuePair& arg1 = args[0];
const InvokeParser::TypeValuePair& arg2 = args[1];
const InvokeParser::TypeValuePair& arg3 = args[2];
if((Arg1::name() != arg1.first) ||
(Arg2::name() != arg2.first) ||
(Arg3::name() != arg3.first))
{
#if defined(DEBUG)
::OutputDebugStringW(Sprintf<wchar_t>(
L"Function: "%s":n"
L"%s -> %sn"
L"%s -> %sn"
L"%s -> %sn",
dbg_function_name.c_str(),
Arg1::name().c_str(), arg1.first.c_str(),
Arg2::name().c_str(), arg2.first.c_str(),
Arg3::name().c_str(), arg3.first.c_str()).c_str());
#endif
ASSERT_MESSAGE_AND_RETURN_VALUE(false, "Type mismatch", false);
}
// Call function with 3 args
f(Arg1::convert(arg1.second),
Arg2::convert(arg2.second),
Arg3::convert(arg3.second));
return true;
}
};
template<>
struct FunctionCaller<4>
{
template<typename Function>
static bool call(Function f, const InvokeParser::ArgsContainer& args
#if defined(DEBUG)
, const std::wstring& dbg_function_name
#endif
)
{
typedef FlashType<typename boost::function_traits<Function>::arg1_type> Arg1;
typedef FlashType<typename boost::function_traits<Function>::arg2_type> Arg2;
typedef FlashType<typename boost::function_traits<Function>::arg3_type> Arg3;
typedef FlashType<typename boost::function_traits<Function>::arg4_type> Arg4;
const InvokeParser::TypeValuePair& arg1 = args[0];
const InvokeParser::TypeValuePair& arg2 = args[1];
const InvokeParser::TypeValuePair& arg3 = args[2];
const InvokeParser::TypeValuePair& arg4 = args[3];
if((Arg1::name() != arg1.first) ||
(Arg2::name() != arg2.first) ||
(Arg3::name() != arg3.first) ||
(Arg4::name() != arg4.first))
{
#if defined(DEBUG)
::OutputDebugStringW(Sprintf<wchar_t>(
L"Function: "%s":n"
L"%s -> %sn"
L"%s -> %sn"
L"%s -> %sn"
L"%s -> %sn",
dbg_function_name.c_str(),
Arg1::name().c_str(), arg1.first.c_str(),
Arg2::name().c_str(), arg2.first.c_str(),
Arg3::name().c_str(), arg3.first.c_str(),
Arg4::name().c_str(), arg4.first.c_str()).c_str());
#endif
ASSERT_MESSAGE_AND_RETURN_VALUE(false, "Type mismatch", false);
}
// Call function with 4 args
f(Arg1::convert(arg1.second),
Arg2::convert(arg2.second),
Arg3::convert(arg3.second),
Arg4::convert(arg4.second));
return true;
}
};
И сам Function
:
struct IFunction
{
virtual bool call(const InvokeParser& parser) = 0;
virtual ~IFunction()
{
}
};
template<typename FunctionPointer>
struct Function :
public IFunction
{
Function(const std::wstring& function_name, FunctionPointer f)
: f_(f)
, name_(function_name)
{
}
bool call(const InvokeParser& parser)
{
typedef typename boost::remove_pointer<FunctionPointer>::type FunctionType;
enum { ArgsCount = boost::function_traits<FunctionType>::arity };
ASSERT_MESSAGE_AND_RETURN_VALUE(
name_ == parser.function_name(),
"Incorrect function name",
false);
ASSERT_MESSAGE_AND_RETURN_VALUE(
ArgsCount == parser.arguments_count(),
"Incorrect function arguments count",
false);
return FunctionCaller<ArgsCount>::template call<FunctionType>(
f_, parser.arguments()
#if defined(DEBUG)
, name_
#endif
);
}
protected:
FunctionPointer f_;
std::wstring name_;
};
template<typename FunctionPointer>
IFunction* CreateFunction(const std::wstring& name, FunctionPointer f)
{
return new Function<FunctionPointer>(name, f);
}
Спасибо за внимание!
Автор: Door