Не знал, как поточнее назвать статью, но хотелось бы разобрать одну маленькую задачку, которая звучит следующим образом:
На вход подаётся отформатированная некоторым образом строка, в которой указаны имя функции, её аргументы и типы аргументов. Нужно иметь возможность вызвать соответствующий обработчик функции, корректно передав все аргументы.
Например, так 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 -> %s\n", 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 -> %s\n" L"%s -> %s\n", 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 -> %s\n" L"%s -> %s\n" L"%s -> %s\n", 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 -> %s\n" L"%s -> %s\n" L"%s -> %s\n" L"%s -> %s\n", 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); }
Спасибо за внимание!
ссылка на оригинал статьи http://habrahabr.ru/post/223865/
Добавить комментарий