Как стать автором
Обновить

Вызов функции, соответствующей заданной строке

Время на прочтение14 мин
Количество просмотров15K
Привет!
Не знал, как поточнее назвать статью, но хотелось бы разобрать одну маленькую задачку, которая звучит следующим образом:
На вход подаётся отформатированная некоторым образом строка, в которой указаны имя функции, её аргументы и типы аргументов. Нужно иметь возможность вызвать соответствующий обработчик функции, корректно передав все аргументы.

Например, так 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);
	}
};


Как видно, имя всех числовых типов, в случае ActionScriptnumber. Для преобразования со строки, сначала аккуратно преобразовываем в 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 генерирует пакет параметров 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);
}



Спасибо за внимание!

UPD: забыл добавить - в самом новом, грядущем, 14м стандарте есть(будет) std::make_index_sequence, который делает всё то же, что и generate_sequence.
Теги:
Хабы:
Всего голосов 32: ↑25 и ↓7+18
Комментарии12

Публикации

Истории

Работа

Программист C++
103 вакансии
QT разработчик
3 вакансии

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань