17 April 2012

Мощь и гибкость

Довольно часто в практике использования C++ сталкиваешься с такими кейсами, отработав которые, понимаешь за что ты любишь этот язык.


Вот один из таких примеров.

Есть реализация некоего циклического FIFO буфера. Чтение из и запись в этот буфер происходит порционно, причем операции чтения иногда сдвигают указатель, а иногда нет. Для операций чтения и записи реализованы соответствующие вспомогательные классы -- итератор чтения и итератор записи, конструкторы которых принимают на вход собственно сам буфер, размер блока для операции и флаг "сдвигать или не сдвигать указатель" для итератора чтения. Итераторы не копируемые, используется RAII -- в своих деструкторах итераторы проверяют, что пользователь вычитал указанный объем данных, и, при необходимости, сдвигают соответствующий указатель внутри буфера.

И все было хорошо, ровно до момента, когда был написан код, в котором стало понятно, что на стек итератор больше не положишь -- функции требовался список итераторов, количество элементов в котором определялось уже во время исполнения. В ход пошел наш велосипед под называнием Utils::ManagedList -- контейнер, хранящий указатели и отвечающий за их удаление (аналоги есть в boost pointer container). Все хорошо, но функция эта относилась к 1% кода, в котором производительность была важной характеристикой, а используемое решение приводило к довольно интенсивной работе с кучей, которую сильно хотелось избежать.


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

В конце-концов решение было найдено. В утилитах появились новые классы, реализующие концепцию "in place" ManagedList -- от существующего ManagedList они отличались тем, что использовали размещающий new, т.е. метод Add вместо указателя на объект, принимает набор аргументов, которые используются для вызова конструктора на регионе памяти внутри контейнера. Идея была реализована в двух разновидностях, в одном варианте в качестве внутреннего буфера использовался char массив, т.е. объекты фактически размещаются прямо на стеке, а их количество указывается как шаблонный параметр контейнера, второй вариант -- размещение в std::vector<char>. В случаях, когда размещаемые в "in place" контейнере объекты не большие по размеру, а их количество разумно ограничено -- можно организовать их хранение прямо на стеке. Если нет -- используется "резиновый" вариант с кучей, причем время жизни такому хранилищу можно дать большое, чтобы не сорить в куче каждый раз.
Разумеется, сам класс является шаблонным и способен работать с любым указанным типов, и, разумеется, метод Add() принимает аргументы как шаблоны, что позволяет вызывать любые варианты конструктора для используемого типа.

Получилось красиво, надежно, универсально, удобно, строго типизированно и совершенно безопасно в использовании.
Вот за такое я и люблю этот язык.

No comments:

Post a Comment