Виклик функції, відповідної заданої рядку

    Привіт!
Не знав, як точніше назвати статтю, але хотілося б розібрати одну маленьку задачу, яка звучить наступним чином:
 
На вхід подається відформатована деяким чином рядок, в якій вказані ім'я функції, її аргументи і типи аргументів. Потрібно мати можливість викликати відповідний обробник функції, коректно передавши всі аргументи.
Наприклад, так 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
, який буде зберігати ім'я функції і масив пар тип аргументу-його значення :
 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
).
 TypeHelper
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
:
 Повні спеціалізації TypeHelper для 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
:)
 
 InvokeParser
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_;
};

 
 TypeHelper
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
замінює розпакування… :
 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
:
 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);
}

 
 
Спасибі за увагу!
    
Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.