Pull to refresh

Variadic Templates, Low Coupling и немного размышлений

Reading time16 min
Views12K
Каждый программист, наверняка, сталкивался с ситуацией, когда в приложении имеется набор классов (возможно, сервисных), которые используются во многих участках программы. И вроде бы всё ничего, но как только появлялась необходимость менять эти классы, это могло негативно влиять и на вызывающий код. Да, как и указано в заголовке, речь в статье пойдет о том самом паттерне «Low Coupling».



Проблема не нова и давно известна. Путей ее решения может быть несколько, все зависит от предметной области. Я предлагаю читателю возможное решение, которое я нашел, занимаясь прикладной задачей. Как идеалиста, найденное решение меня устроило не полностью. Так же, оно было спроектировано в бОльшей степени от желания воспользоваться новыми возможностями стандарта C++11. Естественно, все написанное подлежит обсуждению, а возможно, кто-то предложит более стройный вариант.

Формулировка задачи




В системе имеются различные устройства. Например, температурные датчики, датчики скорости, сигнализация, светофор и прочие. Как читатель и предполагает, все эти устройства в программе представлены наследниками от базового класса. Разумеется, у каждого наследника свои специфические функции-члены.
-Все как обычно. В чем проблема-то?

Проблема заключается в том, что систем несколько. В каждой такой системе устройства могут различаться как по составу, так и по функциональности (расширять или наоборот, сужать). Скажем, в одной системе нет светофора, а в другой, все как и в первой, но сигнализация имеет несколько режимов срабатываний. Возможный случай я привел на диаграмме.



Видно, что конкретная реализация SyncBoard для первой системы, только переопределяет метод запроса скорости, тогда как для второй системы еще добавляются новые методы. Таким образом, вызывающему необходимо знание о конкретном типе каждого устройства, с которым он работает. Но вызывающий не меняется от системы к системе. Его основная логика неизменна, меняются только способы взаимодействия с устройствами.
-Разумеется, если вызывающему необходимо задействовать новый функционал устройства, то он просто обязан знать его конкретный тип.

Но давайте посмотрим на это с другой стороны. Имеется некий модуль/класс, который в одной системе использует тип SyncBoard_System1, а во второй, соответственно, SyncBoard_System2. Как я сказал выше, данный модуль/класс не меняет своей логики в этих двух системах. Меняется только взаимодействие с устройством. Какие есть варианты?
1) Скопировать данный модуль и использовать новый способ взаимодействия. В разных системах использовать разные копии модулей.
-Спасибо, посмеялся.

2) Вынести взаимодействие с устройством в отдельную виртуальную функцию. В наследниках переопределять взаимодействие с устройством.
-Уже лучше. Только ты на каждое устройство, с которым взаимодействует класс будешь делать по виртуальной функции? С новыми устройствами должны будут появляться новые виртуальные функции в заинтересованных классах.

3) Инкапсулировать взаимодействие с устройством в отдельный класс и использовать его как стратегию.
-Звучит как в умных книжках. Но ведь для нового функционала устройства, стратегия тоже должна его реализовать. То есть интерфейс стратегии будет меняться, и тогда все равно понадобиться знать конкретную реализацию(тип) стратегии.

Конкретика не ушла, но количество связей уменьшено. Вместо того, чтобы каждый вызывающий модуль был завязан на конкретную реализацию устройства (которых множество), можно определить (стратегию), который будет делегировать вызовы этим устройствам.
-Очень похоже на Mediator или Facade.

Только внешне. Общее у этих паттернов и моей задачей то, что вместо множества связей, создается одна единственная связь с «». Пока что остановимся на том решении, что необходим класс, который скрывает в себе все конкретные реализации устройств и предоставляет интерфейс клиентам для взаимодействия с ними.

Поиск варианта реализации


Вариант 1 – в лоб.


Менеджер хранит в себе список всех устройств и предоставляет соответствующий интерфейс.



-А если у тебя в системе более одного температурного датчика? Функция getTemperature() какую вернет?

Справедливое замечание. Получается необходимо в каждую такую функцию еще добавить параметр — идентификатора устройства. Поправим.



Вариант этот мне сразу пришел в голову, т.к. самый простой. И это, пожалуй, его последний плюс. Давайте представим, что нас ждет при добавлении нового устройства. Помимо создания самого класса нового устройства, мы должны будем отобразить все его функции и в менеджере (и делегировать их, естественно). А если это устройство вдруг добавляется сразу во все системы, то еще и добавить все эти функции во все конкретные реализации менеджеров всех систем. «Копи-паст» в чистом виде.
-Не красиво, но не смертельно.

Другой сценарий. Две системы абсолютно одинаковы. Отличаются только отсутствием одного единственного устройства. В таком случае, мы уже не можем использовать написанный класс менеджера, т.к. в нем присутствуют функции, которые не нужны. Ну, точнее можем, но есть мнение, что это неправильно. Остается либо наследоваться и заменять пустой реализацией эти методы, либо копировать класс и удалять лишнее. Ни то ни другое меня не устроило.

Вариант 2 – куча case’ов.


А что, если мы сделаем у менеджера один единственный виртуальный метод, внутри которого будет происходить делегирование?
-Ты про разные параметры функций и разные возвращаемые значения не забыл?

Для этого мы создадим базовый класс данных, через который вызывающий код будет передавать и получать параметры.



Пример реализации
class IDevData
{
public:
	IDevData();
	virtual ~IDevData();

	virtual int getID() = 0;
};

class DevManager_v2 : public IDeviceManager
{
public:
	bool initialize()
	{
		// очень полезная инициализация
	}

	// супер метод
	virtual void callMethod(int dev_id, IDevData& data)
	{
		switch (data.getID())
		{
		case DATA_SYNC_BOARD_GET_STATE:
			// вызываем у устройтсва с идентификатором dev_id метод getState();
			// результат складываем обратно в IDevData
		break;
		case DATA_SYNC_BOARD_GET_VELOCITY:
			// ...
		break;
			// ... etc
		}
	}
};


Чего мы добились? У нас теперь только один виртуальный метод. Все изменения происходят только в нем, при добавлении/удалении устройств от системы к системе исчезают автоматом ненужные функции. Вызывающим классам вообще не нужно знать конкретных реализаций менеджера! Им нужно только знать метод callMethod и …
-Да, да! И конкретный тип IDevData для каждого вызова. Если ты бежал раньше от связывания с конкретными реализациями устройств, то пришел к связыванию к конкретным реализациям оберток IDevData. Забавненько.

Какой-то замкнутый круг. Приехали к тому же, с чего начали. Перед тем как вызвать один единственный метод у менеджера, вызывающему нужно будет точно знать какой конкретно тип IDevData создавать. И чем это отличается от ситуации, когда вызывающий знал конкретный тип устройств? Да ничем!

Вариант 3 – C++11


Идея единственной функции callMethod() мне понравилась. Но проблема с передачей и возвращением параметров сводили все старания на нет. Было бы замечательно, если мы смогли бы передавать в эту единственную функцию любые параметры в любом количестве и могли бы получать из нее же любой тип возвращаемого значения…
-Да все уже поняли, что ты говоришь о шаблонах и C++11. Давай, рассказывай, как космические корабли бороздят…©

Новый стандарт как раз предоставляет такие возможности. Стало ясно, что функция callMethod должна быть шаблонной и иметь следующий прототип:
template <typename ResType, typename ... Args>
ResType callMethod(int dev_id, Args&& ... args);

Вызывающему коду теперь ничего не надо знать, кроме менеджера, типов параметров и возвращаемых значений (что и так само-собой разумеющееся)!
— А как ты будешь решать ситуацию, когда в одном классе есть две одинаковые по сигнатуре функции? Судя по твоему ходу мысли, ты хочешь обращаться куда-то (куда?) по dev_id(который означает конкретный класс) и просто передавать все параметры Args&&… кому-то (кому?).

Действительно, это создает проблему. Есть два варианта ее решения. Добавить еще один параметр – int method_id, что мне совсем не нравится, либо придать иной смысл параметру — int dev_id. Назовем его, скажем, command_id, а означать он будет теперь — конкретный метод конкретного класса. То есть некий идентификатор пары Класс->метод. Таким образом значений этих command_id будет ровно столько же, сколько методов у всех классов устройств. По хорошему, конечно, это нужно превратить в перечисление, но не будем заостряться на этом. Теперь по поводу «куда обращаться с command_id» и «кому передавать Args&&». Подсказку нам дает уже сам параметр command_id. Предполагается некая коллекция методов, обращение к которой идет по command_id. Иными словами, необходима следующая схема:
1) Создать хранилище для любой функций с любой сигнатурой
2) В callMethod изымать из хранилища по ключу command_id нужный объект и передавать все параметры
3) Profit!
-Спасибо, КЭП.

Пункт 1 уже решен до меня. В частности, на хабре так же была статья про type erasure. Прочитал и немного видоизменил под свои нужды. Спасибо rpz и прочие источники.

Для тех, кому лень или некогда перечитывать, вкратце расскажу, как это работает. Прежде всего, нам необходим базовый класс с одной полезной виртуальной функцией для проверки типов.

class base_impl 
{
public:
	virtual std::type_info const& getTypeInfo() const = 0;
};


Далее, создаем наследника – шаблонный класс. В него можно передать любую функцию. Чтобы не создавать разные шаблоны для разных функций (метод класса, или простая функция) я решил использовать уже готовую std::function. Все, что требуется от этого класса – перегрузить оператор operator(), в который и передаются параметры для делегирования вызова.
Шаблон type_impl
template <typename ResType, typename ... Args>
class type_impl : public base_impl
{
	typedef std::function<ResType(Args ...)> _Fn;
	_Fn _func;
public:
	type_impl(std::function<ResType(Args ...)> func) : 
		_func(func) {} 
	std::type_info const& getTypeInfo() const
	{
		return typeid(_Fn);
	}
	ResType operator()(Args&& ... args)
	{
		return _func(std::forward<Args>(args)...);
	}
};


На данный момент, уже есть схема, которая позволяет сложить любую функцию в контейнер через указатель на класс base_impl. Но как через этот указатель добраться до вызова operator()? Необходимо преобразование типа. Для этого у нас и существует метод getTypeInfo(). Чтобы эту подноготную скрыть, а так же необходимость вручную писать каждый раз, при добавлении функции в контейнер, шаблон type_impl, создадим последний класс с одной маленькой хитростью – шаблонным конструктором.
FuncWrapper
class FuncWrapper
{
	std::unique_ptr<base_impl> holder;
public:
	template <typename ResType, typename ... Params>
	FuncWrapper(std::function<ResType(Params ...)> func) : 
		holder(new type_impl<ResType, Params ...>(func)) {}
	~FuncWrapper() {}

	template <typename ResType, typename ... Args>
	ResType call(Args&& ... args)
	{
		typedef std::function<ResType(Args ...)> _Fn;
		if (holder->getTypeInfo() != typeid(_Fn))
			throw std::exception("Bad type cast");

		type_impl<ResType, Args ...>* f = 
			static_cast<type_impl<ResType, Args ...>*>(holder);	
		return (*f)(std::forward<Args>(args)...);		}
	}
};


Добавляем к нему шаблонный метод call() и в нем делегируем вызов сохраненному type_impl.
Пример использования
class FuncWrapper
{
private:
	// базовый класс для обертки над callable object.
	// необходим только для того, чтобы иметь указатель на любой тип
	class base_impl 
	{
	public:
		virtual std::type_info const& getTypeInfo() const = 0;
	};


	// в этом классе хранится секрет фокуса. 
	// объявляем шаблонный класс - наследник от base_impl. 
	// теперь чем бы мы не параметризировали type_impl, его ссылку
	// всегда можно присвоить указателю base_impl
	// параметризуется шаблон возвращаемым значением функции - ResType
	// и аргументами функции - Args...
	template <typename ResType, typename ... Args>
	class type_impl : public base_impl
	{
		typedef std::function<ResType(Args ...)> _Fn;
		_Fn _func; // тут то и хранится переданный нам колбек!
	public:
		// я не стал изголяться, и сразу решил в конструктор отправлять std::function
		// зачем мне реализовывать функционал std::function самому?
		type_impl(std::function<ResType(Args ...)> func) : 
			_func(func) {}

		// та самая функция предка, которая выполняет проверку типов.
		// с ее помощью, мы гарантируем корректное приведение типов.
		// ну или exception.
		std::type_info const& getTypeInfo() const
		{
			return typeid(_Fn);
		}

		// ну а тут, очевидно, идет делегирование вызова сохраненному колбеку.
		ResType operator()(Args&& ... args)
		{
			return _func(std::forward<Args>(args)...);
		}
	};

	std::unique_ptr<base_impl> holder;
public:
	// чудо конструктор, в который мы передаем свжеиспеченную std::function
	// сами знаете, что в нее можно уложить что угодно
	template <typename ResType, typename ... Params>
	FuncWrapper(std::function<ResType(Params ...)> func) : 
		holder(new type_impl<ResType, Params ...>(func)) {}
	~FuncWrapper() {}

	// шаблонный метод, через который получаем доступ к сохраненной функции
	template <typename ResType, typename ... Args>
	ResType call(Args&& ... args)
	{
		typedef std::function<ResType(Args ...)> _Fn;
		if (holder->getTypeInfo() != typeid(_Fn))
			throw std::exception("Bad type cast");
		// если типы совпали, то делегируем вызов
		type_impl<ResType, Args ...>* f = 
			static_cast<type_impl<ResType, Args ...>*>(holder.get());	
		return (*f)(std::forward<Args>(args)...);
	}
};

// предположим, что класс test, какое-то устройство
class test
{
public:
	test() {}
	int fn1(int a)
	{
		cout << "test::fn1!!! " << a << endl;
		return ++a;
	}

	int fn2(int a, int b)
	{
		cout << "test::fn2!!! " << a << endl;
		return a + 2;
	}
	int fn3(int a, int b)
	{
		cout << "test::fn3!!! " << a << endl;
		return a + 3;
	}
};

class IDeviceManager
{
protected:
	std::map<int, FuncWrapper*> m_funcs;
public:
	virtual ~IDeviceManager() {};
	virtual void initialize() = 0;

	template <typename ResType, typename ... Args>
	ResType callMethod(int command_id, Args&& ... args)
	{
		// очень не аккуратный код – только для демонстрации идеи.
		return m_funcs[command_id]->call<ResType>(std::forward<Args>(args)...);
	}
};

const int FN1_ID = 0;
const int FN2_ID = 1;
const int FN3_ID = 2;

class DevManager_v3 : public IDeviceManager
{
	std::unique_ptr<test> m_test_ptr;
public:
	void initialize()
	{
		// очень полезная инициализация
		m_test_ptr.reset(new test);

		std::function<int(int)> _func1 = std::bind(&test::fn1, m_test_ptr.get(), std::placeholders::_1);
		std::function<int(int, int)> _func2 = std::bind(&test::fn2, m_test_ptr.get(), std::placeholders::_1, std::placeholders::_2);
		std::function<int(int, int)> _func3 = std::bind(&test::fn3, m_test_ptr.get(), std::placeholders::_1, std::placeholders::_2);

		// складываем в коллекцию все методы
		m_funcs[FN1_ID] = new FuncWrapper(_func1);
		m_funcs[FN2_ID] = new FuncWrapper(_func2);
		m_funcs[FN3_ID] = new FuncWrapper(_func3);
	}

	~DevManager_v3()
	{
		// тут подчищаем коллекцию
	}
};


int _tmain(int argc, _TCHAR* argv[])
{
	
	DevManager_v3 dev_manager;
	dev_manager.initialize();

	// Вуая-ля! К любому методу можно обратится через его идентификатор.
	dev_manager.callMethod<int>(FN1_ID, 1);
	dev_manager.callMethod<int>(FN2_ID, 2, 3);
	dev_manager.callMethod<int>(FN3_ID, 4, 5);

	getchar();
}


Ого! У нас теперь есть один единственный виртуальный метод initialize(), который создает все необходимые устройства для данной системы и помещает их методы в коллекцию. Вызывающему даже не нужно знать конкретный тип менеджера. Шаблонный метод callMethod() все сделает за нас. Для каждой конкретной системы, создается нужный экземпляр IDevManager с помощью, скажем, <фабрики>. Вызывающему нужно иметь только указатель на предка IDevManager.
-Кажется, ты достиг своей цели.

Да, но появляются новые недостатки и они, пожалуй, имеют более весомые негативные последствия, по сравнению с первыми вариантами. Код становится не безопасным!
Во-первых, посмотрите внимательно на callMethod(). Если мы передадим ключ, которого нет в коллекции, то получим исключение. Само собой, необходимо сначала проверять, имеется ли данный ключ в коллекции. Но что делать, когда выяснилось, что ключа не существует (запрошен не существующий метод)? Генерировать исключение? И самое главное, что на этапе компиляции мы этого отловить не сможем. Такое может произойти, когда в какой-то системе отсутствует устройство, или часть его методов.
Во-вторых, редактор кода не подскажет какие параметры ожидаются на входе callMethod() — не высветит название/тип/количество параметров. Если мы передадим не тот тип параметра, или неверное количество параметров, нас ждет опять исключение, но уже в методе call() класса test_impl. И снова, мы не сможем это отловить на этапе компиляции. Такое может легко произойти по невнимательности программиста.

Применительно к поставленной задаче, меня это не устроило, по следующим причинам:
— На этапе компиляции всегда известно точное количество классов (соответственно методов), к которым нужен доступ.
— Эти классы меняются только при проектировании разных систем.
Поэтому пришлось начать с нуля.
— «Шо, опять?!» ©


Вариант 4 – конечный?


К нему я пришел, увидев весьма незатейливую конструкцию:
template <class ... T>
class Dissembler : public T ...
{
};

Что тут происходит? Всего-лишь множественное наследование. Но похоже, это то, что мне надо. Если я наследуюсь от всех своих классов устройств, у меня автоматом в этом классе появляются все их методы. Недостатки третьего варианта исчезают.
— Святая простота. У тебя это будет работать до тех пор, пока тебе не придется наследоваться от двух одинаковых классов. Либо в двух классах будут одинаковые функции (ромбовидное наследование).

Значит нужно каким-то образом сделать методы уникальными, даже если у них одинаковая сигнатура. И я знал, где есть подсказка. Спасибо, Андрей!
Тут все просто
template <typename T, T t>
struct ValueToType 
{};

template<typename C, class T>
class ClassifiedWrapper
{};

template<typename C, C c, class T>
class ClassifiedWrapper<ValueToType<C, c>, T> : private T
{
public:
	ClassifiedWrapper(T&& fn) : T(std::forward<T>(fn)) {};
	ClassifiedWrapper() = delete;
	virtual ~ClassifiedWrapper() {};

	template <typename ... Args>
	std::result_of<T(Args...)>::type 
	operator()(ValueToType<C, c>&&, Args&& ... args)
	{
		return T::operator()(std::forward<Args>(args) ...);
	};
};


Класс ValueToTypeслужит одной цели – задавать тип в зависимости от значения шаблонного параметра. Класс ClassifiedWrapper – это очередная обертка над «callable object». Его цель – наследоваться от объекта, у которого определен оператор скобки operator(), и делегировать вызов, но с дополнительным параметром, который и вносит «уникальность». То есть:
Пример
class test
{
public:
	test() {}
	int fn1(int a)
	{
		cout << "test::fn1!!! " << a << endl;
		return ++a;
	}

	int fn2(int a, int b)
	{
		cout << "test::fn2!!! " << a << endl;
		return a + 2;
	}
	int fn3(int a, int b)
	{
		cout << "test::fn3!!! " << a << endl;
		return a + 3;
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
test t;
std::function<int(int, int)> _func2 = std::bind(&test::fn2, &t, std::placeholders::_1, std::placeholders::_2);

	ClassifiedWrapper<ValueToType<int, 0>, decltype(_func2)> cw1(std::ref(_func2));
	ClassifiedWrapper<ValueToType<int, 1>, decltype(_func2)> cw2(std::ref(_func2));

	cw1(ValueToType<int, 0>(), 2, 1); // ОК
	cw2(ValueToType<int, 1>(), 3, 4); // ОК

 	cw1(ValueToType<int, 0>(), 1); // не скомпилируется
 	cw2(ValueToType<short, 1>(), 3, 4); // не скомпилируется
}


Функция используется одна и та же, но мы сделали две ее обертки разными с помощью дополнительного параметра.
— И что с этим добром делать?

В итоге у нас есть класс, который позволяет обернуть любую функцию и сделать его уникальным. Помните первоначальную проблему, с чего начался этот вариант? Теперь можно применить тот же фокус с множественным наследованием, но наследоваться уже от CalssifiedWrapper.
Сначала объявим заготовку:
template <class ... T>
class Dissembler
{};


Далее, сделаем частичную специализацию, которая одновременно запустит рекурсию раскрытия пакета параметров
template <typename C, C c, class Func, class ... T>
class Dissembler<ValueToType<C, c>, Func, T ...> :
	protected ClassifiedWrapper<ValueToType<C, c>, Func>, protected Dissembler<T ...>
// тут стоит поянсить, что Dissembler<T ...> раскрывается снова в пару ValueToType<C, c> и Func и остаток T ..., который может быть и пустым. И так далее, вплоть до замыкания (ниже)
{
protected:
	using ClassifiedWrapper<ValueToType<C, c>, Func>::operator();
	using Dissembler<T ...>::operator();
public:
	Dissembler(ValueToType<C, c>&& vt, Func&& func, T&& ... t) :
		ClassifiedWrapper<ValueToType<C, c>, Func>(std::forward<Func>(func)),
		Dissembler<T ...>(std::forward<T>(t) ...) {};

// нельзя создать с конструктором по умолчанию
	Dissembler() = delete;
	virtual ~Dissembler() {};
	
	// основной метод, который и делает всю грязную работу.
	template <typename Cmd, Cmd cmd, typename ... Args>
	std::result_of<Func(Args...)>::type
	call(Args&& ... args)
	{
// здесь мы опять просто делегируем вызов, но уже ClassifiedWrapper’у.
// точнее не делегируем, это же теперь наш собственный унаследованный метод
		return this->operator()(ValueToType<Cmd, cmd>(), std::forward<Args>(args)...);
	};
};


Ну и осталось только сделать остановку рекурсии
template <typename C, C c, class Func>
class Dissembler<ValueToType<C, c>, Func> :
	protected ClassifiedWrapper<ValueToType<C, c>, Func>
{
public:
	Dissembler(ValueToType<C, c>&&, Func&& func) :
		ClassifiedWrapper<ValueToType<C, c>, Func>(std::forward<Func>(func)) {};
	Dissembler() = delete;
	virtual ~Dissembler() {};
};


— Глаза сломаешь. А на пальцах и по понятнее объяснить сможешь?

Идея в основе проста – множественное наследование. Но как только мы встречаем ранее указанные проблемы (два одинаковых класса в цепочке наследования или ромбовидное наследование), то все перестает работать. Для этого мы заводим класс (ClassifiedWrapper), который может как бы «приписать уникальную метку» (на самом деле ничего он не приписывает, это я так красиво выразился) любой функции. При этом и сам ClassifiedWrapper является, естественно, уникальным (опять же, понятно, что при разных параметрах шаблона). Далее, мы просто создаем «статический список» таких уникальных функций обернутых в ClassifiedWrapper, и наследуемся от всех них. Фух, проще, наверное, не смогу объяснить. Вообще, примененный мною фокус с Variadic Template, много где описан. В частности на хабре.
-А почему в замыкании рекурсии нет метода call?

Потому что нет смысла огород городить вокруг одной единственной функции. То есть если кто-то захочет использовать Dissembler не для множества функций, а для одной – то затея эта не имеет смысла. Вот как предполагается все это хозяйство использовать:
Как правильно
int _tmain(int argc, _TCHAR* argv[])
{
	test t;
	std::function<int(int, int)> _func2 = std::bind(&test::fn2, &t, std::placeholders::_1, 
		std::placeholders::_2);

	std::function<int(int, int)> _func3 = std::bind(&test::fn3, &t, std::placeholders::_1,
		std::placeholders::_2);



	Dissembler<
		ValueToType<int, 0>, decltype(_func2),
		ValueToType<char, 'a'>, decltype(_func3)
	> dis(
		ValueToType<int, 0>(), std::move(_func2), 
		ValueToType<char, 'a'>(), std::move(_func3)
		);

	dis.call<int, 0>(0, 1);
	dis.call<char, 'a'>(0, 1);

	getchar();
}


Я намеренно указал для демонстрации два разных типа «идентификатора» функций – ValueToType<int, 0> и ValueToType<char, ‘a’>. В реальной задаче, вместо «непонятного» int и номера метода, гораздо нагляднее использовать перечисления с вразумительными названиями элементов. Работает все достаточно наглядно – указываем параметрами шаблонной функции call тип идентификатора и его значение, и передаем параметры.
В сравнении с предыдущим вариантом, удалось добиться того, что ошибка в количестве параметров или в значении ключа приводят к ошибкам времени компиляции. А значит, для конечной цели, когда количество классов известно заранее и не меняется во время выполнения программы, задача решена.
Конечно, неопытному глазу, а так же человеку, который недолюбливает шаблоны, увидеть генерируемую ошибку компиляции будет потрясением (в случае неверно указанных параметров). Но это гарантирует, что ошибка привлечет внимание и она не произойдет в run-time, а это, я считаю, стоит проделанной работы.
-И как в итоге будет выглядеть твой IDevManager?

Почти так же, как и в варианте 3, но с маленькими отличиями.
Пример
typedef	Dissembler<
		ValueToType<int, 0>, std::function<int(int, int)>,
		ValueToType<int, 1>, std::function<int(int, int)>
		> SuperWrapper;

class IDeviceManager
{
protected:
	std::unique_ptr<SuperWrapper> m_wrapperPtr;
public:
	virtual ~IDeviceManager() {};
	virtual void initialize() = 0;

	template <int command_id, typename ResType, typename ... Args>
	ResType callMethod(Args&& ... args)
	{
		return m_wrapperPtr->call<int, command_id>(std::forward<Args>(args)...);
	}
};

class DevManager_v4 : public IDeviceManager
{
	std::unique_ptr<test> m_test_ptr;
public:
	void initialize()
	{
		// очень полезная инициализация
		m_test_ptr.reset(new test);
		std::function<int(int, int)> _func2 = std::bind(&test::fn2, m_test_ptr.get(), std::placeholders::_1, std::placeholders::_2);
		std::function<int(int, int)> _func3 = std::bind(&test::fn3, m_test_ptr.get(), std::placeholders::_1, std::placeholders::_2);

		m_wrapperPtr.reset(new SuperWrapper(
			ValueToType<int, 0>(), std::move(_func2),
			ValueToType<int, 1>(), std::move(_func3)
			));
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	DevManager_v4 v4;
	v4.initialize();

	v4.callMethod<1, int>(0, 1);
	v4.callMethod<0, int>(10, 31);

	getchar();
}


Определения SuperWrapper (для каждой системы свой собственный), придется вынести в отдельный заголовочный файл. И разделять каждое определение #ifdef’ами, чтоб в нужном проекте подключался «правильный» SuperWrapper. Именно по этой причине, я поставил знак вопроса при написании варианта 4. Конечный?
Tags:
Hubs:
+11
Comments28

Articles

Change theme settings