понедельник, 15 августа 2011 г.

Фабрика объектов и статическая библиотека

Понадобилось мне как-то вычитывать строчки из файла и создавать на основе этих строк объекты C++. Решается эта задача фабриками объектов.

Сел и написал свою простую фабрику. Может недостаточно гибкую, но с моей задачей справляется «на ура». Вот она (factory.h):

#ifndef FACTORY_H
#define FACTORY_H

#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <map>
#include <string>

template <class T, class D>
class Factory : private boost::noncopyable
{
	public:
		typedef boost::shared_ptr<T> Product;
		typedef Product (*CallBack) (D);
		typedef std::map<std::string, CallBack> FactoryCont;

	private:
		static Factory<T, D>* _instance;
		static boost::mutex _guard;

		FactoryCont _factories;

		Factory () {};

	public:
		static Factory<T, D>* get_instance ()
		{
			if (! _instance) {
				boost::mutex::scoped_lock l(_guard);
				if (! _instance) _instance = new Factory<T, D>;
			}
			return _instance;
		}

		bool reg (const std::string& name, CallBack lathe)
		{
			return _factories.insert(
					typename FactoryCont::value_type(name, lathe)).second;
		}

		Product create (const std::string& name, D data) const
		{
			typename FactoryCont::const_iterator p = _factories.find(name);

			if (p == _factories.end())
				throw std::runtime_error(
					"can't find factory for '" + name + "'");

			return (p->second)(data);
		}
};

template<class T, class D>
Factory<T, D>* Factory<T, D>::_instance = NULL;

template<class T, class D>
boost::mutex Factory<T, D>::_guard;

#endif /* FACTORY_H */

Ну и простой пример её использования.

fruitfactory.h:
#ifndef FRUITFACTORY_H
#define FRUITFACTORY_H

#include <string>

#include "factory.h"
#include "fruitbase.h"

typedef Factory<FruitBase, const std::string&> FruitFactory;

#endif /* FRUITFACTORY_H */

fruitbase.h
#ifndef FRUITBASE_H
#define FRUITBASE_H

class FruitBase
{
	private:
	public:
		virtual void eat () const = 0;
};

#endif /* FRUITBASE_H */

apple.h
#ifndef APPLE_H
#define APPLE_H

#include <iostream>

#include "fruitbase.h"
#include "fruitfactory.h"

class Apple : public FruitBase
{
	private:
	public:
		virtual void eat () const;
};

#endif /* APPLE_H */

apple.cpp
#include "apple.h"

namespace
{
	FruitFactory::Product new_apple (const std::string& data)
	{
		return FruitFactory::Product(new Apple);
	}

	const bool registered =
		FruitFactory::get_instance()->reg("apple", new_apple);
}

void Apple::eat () const
{
	std::cout << "I eat an apple!\n";
}


И наконец main.cpp
#include "fruitfactory.h"

int main (int argc, char** argv)
{
	FruitFactory::Product f =
		FruitFactory::get_instance()->create("apple", "red");
	f->eat();

	return 0;
}
CMakeLists.txt
add_executable (main
	main.cpp
	apple.cpp
	apple.h
	factory.h
	fruitbase.h
	fruitfactory.h
)

Если все это просто взять и скомпилировать, то программа работает как ожидается. Яблоко успешно регистрируется в фабрике фруктов и к завершению программы оказывается съеденным. :)
% ./main 
I eat an apple!

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

CMakeLists.txt
add_executable (main
	main.cpp
)
target_link_libraries (main fruits)

add_library (fruits STATIC
	apple.cpp
	apple.h
	factory.h
	fruitbase.h
	fruitfactory.h
)
Запуск:
% ./main 
terminate called after throwing an instance of 'std::runtime_error'
  what():  can't find factory for 'apple'

Похоже, что линковщик не прилинковывает реализацию apple.cpp. Что не удивительно, т.к. класс Apple нигде в программе явно не используется. А вот ведь нужна статическая линковка. Как быть?

UPD:

Решение
Решилась проблема опцией линковщику. Правильный CMakeLists.txt выглядит так:
add_executable (main
	main.cpp
)

add_library (fruits STATIC
	apple.cpp
	apple.h
	factory.h
	fruitbase.h
	fruitfactory.h
)

set (LIBFRUITS -Wl,-whole-archive fruits -Wl,-no-whole-archive)

target_link_libraries (main ${LIBFRUITS})

Опция -Wl,-whole-archive заставляет ld прилинковать библиотеку целиком. А последующая -Wl,-no-whole-archive отменяет включенное поведение. Т.е. целиком вставится только одна библиотека фруктов. :)

http://stackoverflow.com/questions/5693405/specifying-link-flags-for-only-one-static-lib-while-linking-executable

10 комментариев:

  1. Как выход можно зарегить класс явно:
    в apple.h

    FigureFactory::Product new_apple(const std::string& data);

    #define CLASS_REGISTER(name, callback) \
    FigureFactory::get_instance()->reg(#name, callback);

    в apple.cpp

    FigureFactory::Product
    new_apple(const std::string& data)
    {
    return FigureFactory::Product(new Apple);
    }

    В main.cpp

    CLASS_REGISTER(apple, new_apple)

    FigureFactory::Product f =
    FigureFactory::get_instance()->create("apple", "red");

    f->foo();

    ОтветитьУдалить
  2. Такое решение мне известно, но я его не рассматривал, потому что при этом усложняется добавление новых продуктов.

    Если использовать твой вариант, то при добавлении нового продукта придется в main.cpp подключать заголовочный файл, добавлять ещё один CLASS_REGISTER. Т.е. в проекте будет место, где надо перечислить все продукты.

    А в реализации, приведённой в посте, каждый продукт регистрирует себя сам.

    ОтветитьУдалить
  3. Конструктор и деструктор записать бы в приватную область.

    ОтветитьУдалить
  4. @Анонимный
    Вероятно, речь идёт о классе Factory? Конструктор спрятан, а вот деструктор забыл спрятать.

    ОтветитьУдалить
  5. Да так лучше, если вы знакомы с реализацией variant то наверное лучше бы было тогда задачку решать таким образом (вы бы тогда хранили бы просто типы и при необходимости бы создавали бы новый объекты этого типа, а не заставляли бы разработчиков самому писать функцию callback). Это только мое мнение, но так наверное будет красивее

    ОтветитьУдалить
  6. Честно говоря, не понял как тут можно применить variant (речь идет ведь об этом: http://insidecpp.ru/patterns/variant/). В C++ мы же не можем хранить типы в какой либо структуре данных, а потом их использовать, как это можно делать, например, в python.

    ОтветитьУдалить
  7. Читайте, можно все, даже с этого же сайта взял ))
    http://insidecpp.ru/patterns/factory/

    ОтветитьУдалить
    Ответы
    1. Теперь понятно. Это я видел, но мне не понравилось. :)

      Если хочется как либо инициализировать объект при создании, сделать проверки и т.п., то удобно иметь отдельную функцию, в которую это все вынесено.

      И фабрика по ссылке не лишена проблемы, поднятой в этой заметке.

      Удалить
  8. Ответы
    1. Еще хочется добавить, что если в коде на insidecpp захочется инициализировать объект при создании, то придется писать для классов продуктов специальный конструктор с параметром, а это почти тоже самое, что «писать функцию callback».

      Удалить