воскресенье, 6 ноября 2011 г.

Простая библиотека для межпроцессного взаимодействия на базе boost.interprocess

Я давно хотел более подробно разобраться с механизмом IPC, но как-то руки не доходили. Но буквально позавчера я посетил конференцию agilecamp, и после данной конференции у меня возникло желание что нибудь пописать, также хотел попробовать новую штуку - TDD. В общем - одним выстрелом убиваем несколько зайцев.
Для работы нам потребуется:

  • Boost - я использовал версию 1.47.0 (самосборная, на базе gcc-4.4.5)
  • CMake - версия 2.8.2 (из репозитория Debian squeeze)
  • gtest - версия 1.5.0 (из репозитория Debian squeeze)
  • gmoсk - версия 1.4.0 (из репозитория Debian squeeze)
  • QtCreator - версия 2.3.0 (Вручную инсталлированный) - он имеет plugin для поддержки CMake сборки (на самом деле - мой любимый редактор :)).
Итак поехали
Я собираю данную библиотеку в составе более крупного примера (о нем будет в одном из следущих постов), поэтому приведу только часть CMakeLists, которая относится непосредственно  к нашему примеру. Для начала - иерархия проекта:
.
├── CMakeLists.txt
├── cmake
│   └── FindGMock.cmake
├── service
│   ├── CMakeLists.txt
│   └── ...
├── client
│   ├── CMakeLists.txt
│   └── ...
├── common
│   ├── CMakeLists.txt
│   └── ...
├── ipc
│   ├── boost_ipc
│   │   ├── impl
│   │   │   ├── IpcSharedMemoryHeader.h
│   │   │   ├── SharedMemoryIoImpl.cpp
│   │   │   └── SharedMemoryIoImpl.h
│   │   ├── SharedMemoryIo.cpp
│   │   └── SharedMemoryIo.h
│   ├── CMakeLists.txt
│   ├── IoBase.cpp
│   └── IoBase.h

└── tests
    ├── CMakeLists.txt
    ├── ipc
    │   ├── CMakeLists.txt
    │   ├── ClientThreadEmulator.h
    │   ├── IoBaseMock.h
    │   ├── IpcBoostSharedMemoryImplTests.cpp
    │   ├── IpcIoBaseTests.cpp
    │   └── IpcSharedMemoryIoTests.cpp
    └── TestsMain.cpp

На данный момент мы рассматриваем только директории: ipc и tests
Вот корневой CMakeLists.txt:
cmake_minimum_required(VERSION 2.8) project (asio_service) set(Boost_USE_STATIC_LIBS ON) set(Boost_USE_MULTITHREADED ON) set (CMAKE_FIND_LIBRARY_SUFFIXES ".a") set(Boost_ADDITIONAL_VERSIONS "1.47" "1.47.0") find_package(Boost COMPONENTS thread system REQUIRED) include_directories(${Boost_INCLUDE_DIR}) add_definitions(${Boost_DEFINITIONS}) find_package(Threads) include_directories(${CMAKE_CURRENT_SOURCE_DIR}) ######################################################################## add_subdirectory(common) add_subdirectory(ipc) add_subdirectory(client) add_subdirectory(service) set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) add_subdirectory(tests)
_Winnie C++ Colorizer

CMakelists.txt для сборки библиотеки ipc:
set (ipc_srcs IoBase.cpp IoBase.h boost_ipc/SharedMemoryIo.cpp boost_ipc/SharedMemoryIo.h boost_ipc/impl/SharedMemoryIoImpl.cpp boost_ipc/impl/SharedMemoryIoImpl.h boost_ipc/impl/IpcSharedMemoryHeader.h ) add_library(ipc ${ipc_srcs})
_Winnie C++ Colorizer

CMakeLists.txt для тестов:
enable_testing() find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) find_package(GMock REQUIRED) include_directories(${GMOCK_INCLUDE_DIRS})


add_subdirectory(ipc)
_Winnie C++ Colorizer

tests/ipc/CMakeLists.txt:
set (ipc_test_sources IpcIoBaseTests.cpp IpcSharedMemoryIoTests.cpp IpcBoostSharedMemoryImplTests.cpp IoBaseMock.h ClientThreadEmulator.h ) add_executable(ipc_test ${ipc_test_sources} ../TestsMain.cpp) target_link_libraries(ipc_test ${CMAKE_THREAD_LIBS_INIT} ${Boost_LIBRARIES} ${GTEST_LIBRARIES} ${GMOCK_LIBRARIES} ipc rt ) add_test(ipc_test ipc_test)
_Winnie C++ Colorizer

В CMake 2.8.2 не входит скрипт для поиска библиотеки googlemock, поэтому я нашел его на просторах интернета и немного модифицировал, здесь я его приводить не буду, его можно легко написать на основе FindGTest.cmake, который входит в стандартную поставку CMake.

Сначала немного о получившейся библиотеке: данная библиотека намеренно предоставляет упрощенную реализацию IPC взаимодействия, является своего рода фасадом к библиотеке Boost.interprocess:

  • Мы будем использовать блокирующий ввод/вывод с возможностью безопасного прерывания.
  • В качестве передаваемых данных - обычные строки (std::string).
  • За один раз можно передать (принять) только одно сообщение, размером не больше 1024 символа.
  • Упрощаем создание IPC сущностей, вводя понятие клиента и сервера: клиент может соединиться только к запущенному серверу
  • IPC сервер идентифицируется по имени, при создании сервера мы указываем это имя, клиент соединяется с сервером, используя имя (Можно одновременно создать несколько серверов).

Понятно, что данная реализация является упрощенной, я вижу несколько путей развития данной реализации:

  • К серверу может подсоединяться несколько клиентов.
    • Сервер может пул Shared memory сущностей, при получении запроса, он может перемещать соединение в одну из свободных Shared memory сущностей, и работать с клиентом в ней. Данный механизм похож на работу с сокетами.
  • Сделать асинхронный механизм чтения записи, сделать timed механизм чтения-записи.
  • Убрать ограничение в 1024 символа на одно сообщения:
    • Эту задачу можно решить, сделав еще один слой абстракции над уже существующим решением: разбивать сообщение и посылать сообщение по блокам. Возможно есть и другое решение, но я пока про него не знаю.


Итак, поехали! Описание интерфейса:
#ifndef IO_BASE_H #define IO_BASE_H #include <boost/noncopyable.hpp> #include <string> #include <stdexcept> namespace ipc { class exception: public std::runtime_error { public: exception(const std::string & msg)
  std::runtime_error("ipc error: " + msg) { }
}; class IoBase: private boost::noncopyable { public: enum OpenMode { ServerMode, ClientMode }; IoBase(OpenMode mode, const std::string & name); virtual ~IoBase(); OpenMode openMode() const; std::string name() const; std::string recv(); void send(const std::string & msg); void interrupt(); private: OpenMode mOpenMode; std::string mName; virtual std::string doRecv() = 0; virtual void doSend(const std::string & msg) = 0; virtual void doInterrupt() = 0; }; } #endif // IO_BASE_H
_Winnie C++ Colorizer

Данный интерфейс предоставляет всего две функции для самого взаимодействия: send и recv. Предполагается, что данные функции будут осуществлять блокирующий механизм для чтения и записи. Также есть функция, которая прерывает блокирующую операцию. Операция, которая была прервана, должна бросить исключение. Также мы видим, что данный объект конструируется либо в режиме Server, либо в режиме Client. Это сделано для того, чтобы сервер мог создать необходимые сущности для IPC взаимодействия (и корректно их удалить).
Реализация проста, и мы не будем на ней заострять внимание:
#include "IoBase.h" namespace ipc { IoBase::IoBase(IoBase::OpenMode openMode, const std::string & name) : mOpenMode(openMode), mName(name) { } IoBase::~IoBase() { } IoBase::OpenMode IoBase::openMode() const { return mOpenMode; } std::string IoBase::name() const { return mName; } std::string IoBase::recv() { return this->doRecv(); } void IoBase::send(const std::string & msg) { this->doSend(msg); } void IoBase::interrupt() { this->doInterrupt(); } }
_Winnie C++ Colorizer

Мы попробуем реализовать IPC механизм на основе Shared memory - разделяемой памяти. Более подробно данный механизм описан в официальной документации.
Переходим к интерфейсу класса SharedMemoryIo:
#ifndef SHAREDMEMORYIO_H #define SHAREDMEMORYIO_H #include "ipc/IoBase.h" #include <boost/smart_ptr/scoped_ptr.hpp> #include <string> namespace ipc { class SharedMemoryIo: public IoBase { public: SharedMemoryIo(IoBase::OpenMode openMode, const std::string & name); virtual ~SharedMemoryIo(); static void remove(const std::string & name); static bool exists(const std::string & name); private: boost::scoped_ptr<class SharedMemoryIoImpl> mImpl; virtual std::string doRecv(); virtual void doSend(const std::string & msg); virtual void doInterrupt(); }; } /* namespace ipc */ #endif /* SHAREDMEMORYIO_H */
_Winnie C++ Colorizer

Данный класс имеет статическую функцию для безусловного удаления IPC сущностей. Это необходимо в том случае, если наш IPC сервер завершился аварийно и не удалил за собой. Функция remove подчищает оставшиеся IPC объекты. Также есть функция для проверки, что сервер с указанным именем существует.
Данный класс использует идиому PImpl, чтобы скрыть детали реализации. Обратите внимание, что мы также объявляем деструктор, это связано с тем, что шаблон scoped_ptr проверяет, что тип полностью объявлен (в отличие, от того же auto_ptr), и поэтому для корректного уничтожения scoped_ptr необходим нетривиальный деструктор.
Реализация класса тоже проста, мы перенаправляем все запросы к имплементации:
#include "SharedMemoryIo.h" #include "ipc/boost_ipc/impl/SharedMemoryIoImpl.h" namespace ipc { SharedMemoryIo::SharedMemoryIo(IoBase::OpenMode openMode, const std::string & name) : IoBase(openMode, name), mImpl(0) { mImpl.reset(new SharedMemoryIoImpl(this)); } SharedMemoryIo::~SharedMemoryIo() { } std::string SharedMemoryIo::doRecv() { return mImpl->doRecv(); } void SharedMemoryIo::doSend(const std::string & msg) { mImpl->doSend(msg); } void SharedMemoryIo::doInterrupt() { mImpl->doInterrupt(); } void SharedMemoryIo::remove(const std::string & name) { SharedMemoryIoImpl::remove(name); } bool SharedMemoryIo::exists(const std::string & name) { try { SharedMemoryIo checker(IoBase::ClientMode, name); return true; } catch (const std::exception & /* e */) { return false; } } } /* namespace ipc */
_Winnie C++ Colorizer

Проверку, что сервис с указанным именем существует, мы осуществляем просто: пытаемся создать клиента с заданным именем, если это проходит успешно, то значит сервис есть - иначе его нет.

Теперь переходим к внутреннему устройству SharedMemory механизма - к описанию класса SharedMemoryIoImpl:
#ifndef SHAREDMEMORYIOIMPL_H #define SHAREDMEMORYIOIMPL_H #include <string> #include <boost/noncopyable.hpp> #include <boost/interprocess/interprocess_fwd.hpp> #include <boost/smart_ptr/scoped_ptr.hpp> namespace ipc { class IoBase; namespace internal { struct SharedMemoryMapping; } class SharedMemoryIoImpl: private boost::noncopyable { public: SharedMemoryIoImpl(ipc::IoBase * io); ~SharedMemoryIoImpl(); std::string doRecv(); void doSend(const std::string & msg); void doInterrupt(); static void remove(const std::string & name); private: ipc::IoBase * mIo; boost::scoped_ptr<boost::interprocess::shared_memory_object> mMemoryObject; boost::scoped_ptr<boost::interprocess::mapped_region> mRegion; void createServer(); void createClient(); internal::SharedMemoryMapping * getMapping(); }; } #endif // SHAREDMEMORYIOIMPL_H
_Winnie C++ Colorizer
Отдельно стоит рассмотреть структуру SharedMemoryMapping - это специальная структура, которая предоставляет буфер для сообщения, статус IPC, и примитивы для синхронизации:
#ifndef IPCSHAREDMEMORYHEADER_H #define IPCSHAREDMEMORYHEADER_H #include <boost/interprocess/shared_memory_object.hpp> #include <boost/interprocess/sync/interprocess_mutex.hpp> #include <boost/interprocess/sync/interprocess_condition.hpp> namespace ipc { namespace internal { enum SharedMemoryState { ReadyWrite, ReadyRead, ReadyInterrupt }; const size_t MessageBufferSize = 1024; struct SharedMemoryMapping { SharedMemoryMapping() : mutex(), condition(), state(ReadyWrite), messageSize(0) { } boost::interprocess::interprocess_mutex mutex; boost::interprocess::interprocess_condition condition; SharedMemoryState state; size_t messageSize; char msgBuff[MessageBufferSize]; }; } } #endif // IPCSHAREDMEMORYHEADER_H
_Winnie C++ Colorizer
С помощью данной структуры мы сможем осуществлять безопасную доставку сообщения между процессами, а также осуществлять безопасное прерывание любой операции.

Теперь осталось рассмотреть реализацию класса SharedMemoryIoImpl:
#include "SharedMemoryIoImpl.h" #include "ipc/IoBase.h" #include "ipc/boost_ipc/impl/IpcSharedMemoryHeader.h" #include <boost/interprocess/mapped_region.hpp> #include <boost/bind.hpp> using namespace boost::interprocess; namespace ipc { SharedMemoryIoImpl::SharedMemoryIoImpl(ipc::IoBase * io): mIo(io) { BOOST_ASSERT(io && "io object must NOT be NULL"); switch (io->openMode()) { case IoBase::ServerMode: createServer(); break; case IoBase::ClientMode: createClient(); break; default: BOOST_ASSERT(0 && "unknown OpenMode"); break; } } SharedMemoryIoImpl::~SharedMemoryIoImpl() { if (mIo->openMode() == IoBase::ServerMode) { remove(mIo->name()); } } void SharedMemoryIoImpl::createServer() { try { mMemoryObject.reset(new shared_memory_object( create_only, mIo->name().c_str(), read_write)); mMemoryObject->truncate(sizeof(internal::SharedMemoryMapping)); mRegion.reset(new mapped_region(*mMemoryObject, read_write)); void * addr = mRegion->get_address(); /** * @note This placement new is important, * because we must properly initialise mutex and cond_var */ internal::SharedMemoryMapping * mapping = new (addr) internal::SharedMemoryMapping(); } catch (const interprocess_exception & e) { throw exception(std::string("failed to create server: ") + e.what()); } } void SharedMemoryIoImpl::createClient() { try { mMemoryObject.reset(new shared_memory_object( open_only, mIo->name().c_str(), read_write)); mRegion.reset(new mapped_region(*mMemoryObject, read_write)); } catch (const interprocess_exception & e) { throw exception(std::string("failed to create client: ") + e.what()); } } void SharedMemoryIoImpl::remove(const std::string & name) { shared_memory_object::remove(name.c_str()); } internal::SharedMemoryMapping * SharedMemoryIoImpl::getMapping() { void * addr = mRegion->get_address(); return static_cast<internal::SharedMemoryMapping * >(addr); } std::string SharedMemoryIoImpl::doRecv() { internal::SharedMemoryMapping * mapping = getMapping(); scoped_lock<interprocess_mutex> locker(mapping->mutex); while (mapping->state == internal::ReadyWrite) { mapping->condition.wait(locker); } if (mapping->state == internal::ReadyInterrupt) { throw exception("interrupt requested"); } BOOST_ASSERT((mapping->messageSize <= internal::MessageBufferSize) && "shared object corrupted, ipc impossible"); std::string result; std::copy(mapping->msgBuff, mapping->msgBuff + mapping->messageSize, std::back_inserter(result)); mapping->state = internal::ReadyWrite; mapping->condition.notify_one(); return result; } void SharedMemoryIoImpl::doSend(const std::string & msg) { internal::SharedMemoryMapping * mapping = getMapping(); scoped_lock<interprocess_mutex> locker(mapping->mutex); while (mapping->state == internal::ReadyRead) { mapping->condition.wait(locker); } if (mapping->state == internal::ReadyInterrupt) { throw exception("interrupt requested"); } if (msg.size() > internal::MessageBufferSize) { throw exception("message too long"); } std::copy(msg.begin(), msg.end(), mapping->msgBuff); mapping->messageSize = msg.size(); mapping->state = internal::ReadyRead; mapping->condition.notify_one(); } void SharedMemoryIoImpl::doInterrupt() { internal::SharedMemoryMapping * mapping = getMapping(); scoped_lock<interprocess_mutex> locker(mapping->mutex); mapping->state = internal::ReadyInterrupt; mapping->condition.notify_one(); } }
_Winnie C++ Colorizer

Конструктор вызывает создание IPC механизма для сервера или клиента в зависимости от OpenMode.
Функция createServer создает shared memory object, важное замечание - мы должны правильным образом инициализировать mutex и conditional_variable, для этого мы вызываем placement new и конструируем объект в указанной памяти. Изначально, placement new не вызывался, но когда я писал тест для прерывания блокирующей операции, то мой тест работал неправильно. Благодаря написанным ранее тестам, я быстро локализовал ошибку (это плюс в копилку TDD :)).
Функция createClient просто пытается открыть существующий IPC сервер, если ей это не удается, то кидаем исключение.
Функция doRecv осуществляет блокирующее чтение из shared memory: сначала захватываем мьютекс для монопольного доступа к разделяемой памяти, затем запускаем цикл ожидания сообщения (или сигнала прерывания) с помощью условной переменной и блокировки. После того как сигнал получен, мы его анализируем и кидаем исключение (если это сигнал прерывания), либо читаем сообщение из буфера и посылаем сигнал готовности записи.
Аналогично работает функция doSend: она ждет сигнал о том, что очередь пуста (либо операция прервана), и генерирует исключение, либо копирует сообщение и посылает сигнал о его доступности.

Как я уже говорил, я пытался писать данную библиотеку в стиле TDD, ниже я привожу список тестов для всех компонент библиотеки:
Итак, первая группа тестов для интерфейса IoBase:
#include <gtest/gtest.h> #include "IoBaseMock.h" using ::testing::_; TEST(IpcIoBaseTest, DeclareInterfaceForIoBase) { mocks::IoBaseMock ioBaseMock(ipc::IoBase::ServerMode, "some_io_name"); EXPECT_CALL(ioBaseMock, doRecv()); EXPECT_CALL(ioBaseMock, doSend(_)); EXPECT_CALL(ioBaseMock, doInterrupt()); ioBaseMock.recv(); ioBaseMock.send("unused message"); ioBaseMock.interrupt(); ioBaseMock.name(); } TEST(IpcIoBaseTest, ExpectServerMode) { mocks::IoBaseMock ioBaseMock(ipc::IoBase::ServerMode, "some_io_name"); EXPECT_EQ(ipc::IoBase::ServerMode, ioBaseMock.openMode()); } TEST(IpcIoBaseTest, ExpectClientMode) { mocks::IoBaseMock ioBaseMock(ipc::IoBase::ClientMode, "some_io_name"); EXPECT_EQ(ipc::IoBase::ClientMode, ioBaseMock.openMode()); } TEST(IpcIoBaseTest, ExpectTwoDifferentIoNames) { mocks::IoBaseMock ioBaseMock1(ipc::IoBase::ServerMode, "io_name1"); mocks::IoBaseMock ioBaseMock2(ipc::IoBase::ServerMode, "io_name2"); EXPECT_EQ("io_name1", ioBaseMock1.name()); EXPECT_EQ("io_name2", ioBaseMock2.name()); }
_Winnie C++ Colorizer

Первый тест был написан до описания интерфейса IoBase. Здесь мы видим, что первый тест требует наличие функций - то есть первый тест предназначен для описания интерфейса.
Следующие тесты были написан для проверки публичных не-виртуальных методов, эти тесты также были написаны до реализации соответствующих методов.
Декларация mock класса не интересна, она полностью повторяет документацию.

Переходим к тестам для SharedMemoryIo:
#include <gtest/gtest.h> #include <iostream> #include <boost/thread.hpp> #include "ipc/boost_ipc/SharedMemoryIo.h" #include "ClientThreadEmulator.h" TEST(IpcSharedMemoryIoTest, ExpectStaticMethodsOfSharedMemoryObject) { ipc::SharedMemoryIo::remove("unused_server_name"); ipc::SharedMemoryIo::exists("unused_server_name"); } TEST(IpcSharedMemoryIoTest, TryToConstructServerSharedMemoryObject) { const std::string name = "unused_server_name"; ipc::SharedMemoryIo::remove(name); ASSERT_FALSE(ipc::SharedMemoryIo::exists(name)); ipc::SharedMemoryIo memIo(ipc::IoBase::ServerMode, name); ASSERT_TRUE(ipc::SharedMemoryIo::exists(name)); } TEST(IpcSharedMemoryIoTest, ExpectToThrowOnNonExistendSharedMemoryObject) { const std::string name = "nonexistent_server_name"; ipc::SharedMemoryIo::remove(name); EXPECT_THROW(ipc::SharedMemoryIo client(ipc::IoBase::ClientMode, name), ipc::exception); } TEST(IpcSharedMemoryIoTest, ExpectToConnectToExistentSharedMemoryObject) { const std::string name = "test_server"; ipc::SharedMemoryIo::remove(name); { ipc::SharedMemoryIo server(ipc::IoBase::ServerMode, name); ipc::SharedMemoryIo client(ipc::IoBase::ClientMode, name); /** * we does not check any method, * we expect, that both ctors does not throw any exception */ } } TEST(IpcSharedMemoryIoTest, ExpectToSendAndRecvData) { const std::string name = "test_server"; ipc::SharedMemoryIo::remove(name); { ipc::SharedMemoryIo server(ipc::IoBase::ServerMode, name); ipc::SharedMemoryIo client(ipc::IoBase::ClientMode, name); std::string message = "some message"; client.send(message); std::string receivedMessage = server.recv(); EXPECT_EQ(message, receivedMessage); } } TEST(IpcSharedMemoryIoTest, TestForInterruptionOfServerThread) { const std::string name = "test_server"; ipc::SharedMemoryIo::remove(name); ipc::SharedMemoryIo server(ipc::IoBase::ServerMode, name); ClientThreadEmulator client(name); EXPECT_THROW(server.recv(), ipc::exception); }
_Winnie C++ Colorizer

Тесты в данном модуле задают различные аспекты поведения объекта SharedMemoryIo:

  1. Сначала мы требуем, чтобы у класса SharedMemoryIo были две статические функции: remove и exists
  2. Затем мы пытаемся создать SharedMemoryIo в режиме Server: здесь мы сначала пытаемся удалить IPC объект для того чтобы наш тест не сломался, если такой объект был случайно создан ранее. После того как сервер создался - мы проверяем, что он есть с помощью функции exists
  3. В третьем тесте мы проверяем, что клиент не сможет законнектиться к несуществующему серверу : ожидаем исключение.
  4. В четвертом тесте мы последовательно создаем сервер, и клиент. Клиент должен не бросить исключение при попытке соединения с сервером.

Первые 4 теста задавали поведение объектов без самого IPC взаимодействия, следующие два теста как раз описывают передачу данных и прерывание операции:
  1. В пятом тесте мы создаем сервер и клиент, посылаем сообщение серверу. Хотя все операции блокирующие, у нас не возникает блокировки, потому что изначально очередь сообщений пуста, и поэтому клиент без проблем кладет свое сообщение. Когда сервер запускает чтение, то он сразу получает статус сообщения без входа в цикл ожидания.
  2. В последнем тесте мы запускаем сервер в главном потоке, и клиента во вспомогательном потоке. Здесь ожидание сообщения может блокироваться (в зависимости от того, кто раньше стартует: сервер или клиент), но в любом случае мы должны поймать исключение.

Переходим к тесту на имплементацию SharedmemoryIoImpl:
#include <gtest/gtest.h> #include "ipc/boost_ipc/impl/SharedMemoryIoImpl.h" TEST(IpcBoostSharedMemoryImplTests, ExpectAssertionFailureOnNullIoObject) { /// @todo Move this test to single exe without any threads ASSERT_DEATH(ipc::SharedMemoryIoImpl ioImpl(0), 
".*io object must NOT be NULL.*"); }
_Winnie C++ Colorizer

В данном модуле всего один тест (пока что :)), но здесь я запускаю т.н. death test, то есть тест на то что программа закончится фатальной ошибкой: в данном случае я ожидаю, что программа выдаст assertion failure с сообщением io object must NOT be NULL. Это очень удобный механизм для проверки ассертов, рекомендую им пользоваться.

Итак, мы создали библиотеку для IPC взаимодействия на базе механизма shared memory. Данная библиоткека получилась простой в использовании (паттерн Фасад). Необходимо заметить, что boost является кроссплатформенной библиотекой, поэтому данное решение скорее всего будет работать без изменений под другими платформами и операционными системами: win32, sparc-solaris и пр.



Комментариев нет:

Отправить комментарий