28 марта 2006

Фабрики объектов v.2

Немного переделал шаблон фабрик так, чтобы можно было использовать executor-ы с произвольным количеством параметров. Вообще, получилась даже не столько именно "фабрика", сколько контейнер-singleton для регистрации произвольных объектов, не обязательно creator-ов или executor-ов.

Немного изменился синтаксис:
typedef CFactory<IdType, ReturnType(ParameterType1, ParameterType2, ...)> SomeFactory;
...
SomeFactory::Execute(Id)(Parameter1, Parameter2, ...)

25 марта 2006

Object Factory

Задача
В проекте, над которым я сейчас работаю, постоянно стали требоваться так называемые "фабрики объектов" - глобальные объекты, создающие экземпляры каких-либо классов по указанию определенного идентификатора. Например, бизнес-логика, генерирующая отчеты. На входе имеем ID отчета, который нужно создать, и параметры отчета - на выходе нужно получить сформированный отчет. Причем в виде switch (ReportID) { ... } это реализовывать неудобно - по мере добавления отчетов будет расти количество case и все они собраны в одном месте, то есть нужно каждый раз перекомпилировать этот switch... Идеальное решение, чтобы можно было написать реализацию определенного отчета в отдельном модуле (скажем, файле исходного текста), и чтобы он сам "зарегистрировался" в фабрике отчетов под своим ID. Тогда запросив у фабрики отчет по ID, она сама найдет "создателя" нужного отчета.

Решение
Я попытался общий создать шаблон для подобных фабрик объектов. В частном случае от фабрики требуется вызвать функцию, зарегистрированную для определенного ID типа IdType. Функция принимает в качестве параметра объект типа ParameterType, а возвращает значение типа ReturnType.

Например, для отчетов может быть IdType = string, ParameterType = map<string, _variant_t>, ReturnType = shared_ptr<Report>. То есть отчет идентифицируем по его названию, передаем ему параметры, где каждый параметр = имя (string) + значение (_variant_t), а получаем – умный указатель на созданный отчет.

Шаблон я создал следующий: Factory.h

Пример использования:
typedef CFactory
<const string &, shared_ptr<Report>, const map<string, _variant_t> & >
CReportFactory;

shared_ptr<report> Generator(const map<string, _variant_t> &)
{
// generator code
}

// register Generator as "test report"
bool Registered = CReportFactory::Register("test report", Generator);

int main()
{
// fill parameters
map<string, _variant_t> Parameters;
Parameters.insert(...);

// generate report
shared_ptr<Report> TestReport =
CReportFactory::Execute("test report", Parameters);
}

Нюансы
В случае, если все генераторы отчетов компилируются все в одном месте с самой фабрикой (то есть не разнесены по разным библиотекам), то регистрация на фабрике выполняется достаточно просто – введением глобальной переменной:
bool SomeGeneratorRegistered = SomeFactory.Register(SomeID, SomeGenerator);

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

В библиотеке:
bool SomeLibrary1Registered =
SomeGenerator11Registered &&
SomeGenerator12Registered &&
...;

В основном модуле (исполнимый файл или DLL):
extern bool SomeLibrary1Registered;
extern bool SomeLibrary2Registered;
...
bool LibrariesRegistered =
SomeLibrary1Registered &&
SomeLibrary2Registered &&
...;

12 марта 2006

decimal vs float

В C++ если написать 12, то это будет воспринято компилятором как (int)12, а 12.0 дает (float)12.

MS SQL Server 2000 понимает 12 как int, а 12.0 - ...нет, не как float, а как decimal с шестью знаками после запятой.

Мне нужно было посчитать коэффициент 1/12, чтобы результат получился float. 1/12 дает 0, и я использовал 1.0/12.0 наивно полагая, что числа будут восприняты как float. Ан нет, при выполнении запросов вылезали погрешности. Пришлось написать так: 1e0/0.12e2 (или, как вариант - 1e0/12).

07 марта 2006

deque vs vector

Во всех книжках встречаю "если не знаете какой контейнер использовать - используйте vector". Мне вот что-то не взлюбился этот vector, я вот deque люблю. Точнее ее реализацию, где она реализована в виде страниц и вектора указателей на эти страницы. В дальнейшем буду рассматривать только деку с такой реализацией.

Преимущества deque перед vector:
1. Меньше overhead. Вектор растет сразу в два раза, а дека - фиксированными страницами. Если храним мегабайт с копейкой, то вектор будет двухмегабайтный, а дека - мегабайт плюс маленький хвостик.
2. Отстуствие копирования старого содержимого на новое место при расширении контейнера.

Недостатки:
1. Отсутствие доступа к содержимому как к линейному массиву.

Инкремент-декремент итераторов у деки по скорости сравнимы с векторными, произвольный доступ к элементам - аналогично.

А вот отсутствие ненужного копирования при realloc-е у деки существенно улучшает performance.

Достоинство вектора - доступ к содержимому как к линейному массиву. То есть можно накопить кучу данных в векторе, а потом передать все содержимое какой-нибудь API-шной функции типа WriteFile или SendData с параметрами (void *buf, size_t size).

Но это является плюсом вектора только в теории. Сравним два варианта:
1. Накапливаем данные в vector, затем за один вызов "сливаем" их в нашу WriteFile(void *buf, size_t size)
2. Накапливаем данные в deque, затем в цикле "сливаем" часть в буфер ограниченного размера, а потом передаем этот буфер нашей WriteFile.

Предполагаем, что изначально мы не знаем сколько элементов придется поместить в контейнер (иначе, естественно, vector::reserve() всех порвёт).

Пусть в результате в контейнер окажутся помещенными N элементов. Ближайшие к N степени двойки назовем 2n и 2n+1. То есть 2n < N <= 2n+1.

В первом случае на этапе накопления произойдет 1+2+4+8+...+2n = 2n+1-1 копирований. Назовем это количество С1.

Во втором случае будет копирований элементов ровно столько, сколько элементов в контейнере. Происходить все они будут в цикле (когда все они будут копироваться в буфер), а при накоплении данных дек ничего не перекладывает с одного места в другое. Количество копирований C2=N.

С1=2n+1-1 всегда больше С2=N, кроме двух случаев, которые можно особо не учитывать. А если предположить, что в среднем N где-то посередине межде 2n и 2n+1, то второй вариант на 25% быстрее первого.

То есть даже тот факт, что вектор потом можно удобно (без дополнительных копирований) "слить" API-шной функции, не дал вектору преимуществ перед декой.

Исключения: при маленьких N или в случаях, когда N заранее известно - тут вектор выигрывает.