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

в 22:40, , рубрики: c++

Привет!
Не знал, как поточнее назвать статью, но хотелось бы разобрать одну маленькую задачку, которая звучит следующим образом:

На вход подаётся отформатированная некоторым образом строка, в которой указаны имя функции, её аргументы и типы аргументов. Нужно иметь возможность вызвать соответствующий обработчик функции, корректно передав все аргументы.

Например, так 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<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 -> %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:

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js