В нижеследующих пунктах описываются вопросы, которые необходимо было рассмотреть в ходе осуществления циркулярного буфера:
Безопасность потока<circular_buffer
>такая же, как и безопасность потока контейнеров в большинстве реализаций STL. Это означает, что<circular_buffer
>не является полностью безопасным. Безопасность потока гарантируется только в том смысле, что одновременный доступ к отдельным экземплярам<circular_buffer
>безопасен, а одновременный доступ к совместному<circular_buffer
>безопасен.
Если несколько потоков получают доступ к одному<circular_buffer
>, и, по меньшей мере, один из потоков может потенциально записывать, то пользователь несет ответственность за обеспечение взаимного исключения между потоками во время доступа к контейнеру. Взаимное исключение между нитями может быть достигнуто за счет операций обертывания<circular_buffer
>с приобретением и выпуском замка. (См. примерный код Bounded Buffer по адресуcircular_buffer_bound_example.cpp)
Операция перезаписи происходит, когда элемент вставляется в полный<circular_buffer
>— старый элемент перезаписывается новым. Была дискуссия о том, что именно означает «перезапись элемента» во время формального обзора. Это может быть либо разрушение исходного элемента и последующее строительство нового элемента, либо присвоение нового элемента старому.<circular_buffer
>выполняет задание, потому что оно более эффективно.
С точки зрения бизнес-логики хранимого элемента операция разрушения / строительства и назначение обычно означают одно и то же. Однако в очень редких случаях (если таковые имеются) они могут отличаться. Если существует требование о том, чтобы элементы разрушались/конструировались вместо назначения, рассмотрите возможность реализации обертки элемента, которая будет реализовывать оператора назначения, и вместо этого храните обертки. Необходимо отметить, что хранение таких оберток имеет недостаток. Разрушение/конструкция будет вызываться при каждом назначении обертки - не только когда обертка перезаписывается (когда буфер заполнен), но и когда хранимые обертки смещаются (например, в результате вставки в середину контейнера).
Существует несколько вариантов, как справиться, если источник данных производит больше данных, чем может поместиться в буфер фиксированного размера:
- Сообщите источнику данных подождать, пока в буфере не появится место (например, путем исключения переполнения).
- Если самые старые данные являются наиболее важными, игнорируйте новые данные из источника, пока в буфере снова не появится место.
- Если последние данные являются наиболее важными, запишите самые старые данные.
- Пусть производитель несет ответственность за проверку размера буфера перед его записью.
Очевидно, что<circular_buffer
>реализует третий вариант. Но менее очевидно, что он не реализует никакой другой вариант, особенно первые два. Может сложиться впечатление, что<circular_buffer
>должны реализовать первые три варианта и предложить механизм выбора среди них. Это впечатление неправильное.
<circular_buffer
>был разработан и оптимизирован для круговой записи (что означает перезапись самых старых данных при заполнении). Если бы такой механизм управления был включен, это только усложнило бы ситуацию, и использование<circular_buffer
>было бы, вероятно, менее простым.
Кроме того, первые два варианта (а также четвертый вариант) не требуют, чтобы буфер был круговым. Если есть необходимость в первом или втором варианте, рассмотрите возможность реализации адаптера, например, std::vector. В этом случае<circular_buffer
>не подходит для адаптации, потому что, в отличие от std::vector, он несет накладные расходы за его круговое поведение.
При считывании или удалении элемента из пустого буфера буфер должен быть в состоянии уведомить потребителя данных (например, путем исключения из потока данных), что в нем не хранятся элементы.<circular_buffer
>не осуществляет такое поведение по двум причинам:
- Это приведет к повышению производительности.
- Ни один другой контейнер не реализует его таким образом.
Считается ошибкой читать или удалять элемент (например, позвонив<front()
>или<pop_back()
>) из пустого контейнера и из пустого<circular_buffer
>. Потребитель данных должен проверить, не является ли контейнер пустым, прежде чем считывать/удалять его путем тестирования<empty()
>. Однако при чтении из<circular_buffer
>есть возможность полагаться на метод<at()
>, который бросает исключение, когда индекс находится вне диапазона.
Итератор обычно считается недействительным, если элемент, на который указывает итератор, был удален или перезаписан другим элементом. Это определение обеспечивается поддержкой отладки и документируется для каждого метода. Однако некоторые приложения, использующие<circular_buffer
>, могут требовать менее строгого определения: итератор недействителен только в том случае, если он указывает на неинициализированную память.
Рассмотрим следующий пример:
#define BOOST_CB_ENABLE_DEBUG 0
#include <boost/circular_buffer.hpp>
#include <boost/assert.hpp>
#include <assert.h>
int main(int , char* [])
{
boost::circular_buffer<int> cb(3);
cb.push_back(1);
cb.push_back(2);
cb.push_back(3);
boost::circular_buffer<int>::iterator it = cb.begin();
assert(*it == 1);
cb.push_back(4);
assert(*it == 4);
return 0;
}
Итератор больше не указывает на исходный элемент (и считается недействительным с «строгой» точки зрения), но он по-прежнему указывает на то же действительное место в памяти. Это «мягкое» определение недействительности итератора поддерживается<circular_buffer
>, но должно рассматриваться как деталь реализации, а не как полноценная функция. Правила, когда итератор все еще действителен, могут быть выведены из кода вsoft_iterator_invalidation.cpp.
Начиная с Boost 1.54.0 поддержка семантики движения была реализована с помощьюBoost. Перейдитев библиотеку. Если ссылки на значение r доступны<circular_buffer
>, они будут использоваться, но если нет, то используется близкая, но несовершенная эмуляция. На таких компиляторах:
- Некопируемые объекты могут храниться в контейнерах. Они могут быть построены на месте с использованием<
emplace
>или если они поддерживают Boost. Двигайся, двигайся.
- Сами контейнеры не являются подвижными.
- Пересылка аргументов не идеальна.
<circular_buffer
>будет использовать значения r и эмуляции перемещения для типов значений только в том случае, если конструктор перемещения и оператор назначения перемещения типа значения не бросают; или если тип значения не имеет конструктора копии.
Некоторые методы не будут использовать конструктор движения для типа значения вообще, если конструктор бросает. Это необходимо для согласованности данных и избежания ситуаций, когда в афтерере исключение<circular_buffer
>содержит отошедшие объекты вместе с хорошими.
См. документацию для<is_copy_constructible
>,<is_nothrow_move_assignable
>и<is_nothrow_move_constructible
>типов триат. Там вы найдете информацию о том, как сделать конструктор класса, и как сделать некопируемый класс в C++03 и C++98.
Производительность<circular_buffer
>значительно улучшится, если тип значения не имеет ничего, кроме конструктора хода и назначения хода.
Справочная документация<circular_buffer
>содержит примечания типа «Броски: см. Исключения<move_if_noexcept(T&)
>». Это примечание означает следующее:<move_if_noexcept(T&
value)
>вообще не делает исключений, но оно возвращает<value
>в качестве ссылки на значение r, только если класс<T
>имеет конструктор ходов и оператор назначения ходов; или если у него нет конструктора копий. В противном случае<move_if_noexcept(T&
value)
>возвращается<value
>в качестве ссылки.
Это приводит нас к следующей ситуации:
- Если<
value
>имеет не иначе как конструктор хода и не иначе как оператор назначения хода, то никакие исключения не будут брошены вообще.
- Если<
value
>имеет конструктор метательных движений и некоторый конструктор копий, то метод может выбрасывать исключения из конструктора копий.
- Если<
value
>не имеет конструктора копий, то метод может выбросить исключения из конструктора движений.
<move_if_noexcept(T&)
>используетBoost.Move,<is_copy_constructible
>,<is_nothrow_move_assignable
>и<is_nothrow_move_constructible
>тип триат.
<circular_buffer
>не следует использовать для хранения указателей на динамически выделяемые объекты. Когда кольцевой буфер становится полным, дальнейшая вставка перезаписывает сохраненные указатели, что приводит к утечке памяти. Одним из рекомендуемых альтернатив является использование интеллектуальных указателей, напримерУсильте интеллектуальные указатели.
std::auto_ptr
![[Caution]](/img/caution.png) |
Caution |
Особенно опасным считается любой контейнер<std::auto_ptr >. |
![[Tip]](/img/tip.png) |
Tip |
Никогда не создавайте круговой буфер<std::auto_ptr >. См. превосходную книгу Скотта Мейерса «Эффективный STL» для подробного обсуждения. (Мейерс С., Эффективный STL: 50 конкретных способов улучшить использование стандартной библиотеки шаблонов). Эддисон-Уэсли, 2001. |
В то время как внутренние части<circular_buffer
>являются круговыми,итераторы не являются. Итераторы<circular_buffer
>действительны только для диапазона<\[begin(),end()\]
>, поэтому, например: итераторы<(begin()-1)
>и<(end()+
1)
>недействительны.
Чтобы помочь программисту избежать и найти распространенные ошибки, можно включить<circular_buffer
>, чтобы обеспечить своего рода поддержку отладки.
Когда функция отладки включена,<circular_buffer
>поддерживает список действительных итераторов. Как только какой-либо элемент будет уничтожен, все итераторы, указывающие на этот элемент, будут удалены из этого списка и явно признаны недействительными. Поддержка отладки также состоит из множества утверждений<BOOST_ASSERT
>макросов, которые обеспечивают правильное использование<circular_buffer
>и его итераторов во время выполнения. В случае использования недействительного итератора утверждение сообщит об ошибке. Связь явной итерации и утверждений делает очень надежный метод отладки, который улавливает большинство ошибок.
Кроме того, неинициализированная память, выделенная<circular_buffer
>, заполнена значением<0xcc
>в режиме отладки. При отладке кода это может помочь программисту распознать инициализированную память из неинициализированной. Подробно см. исходный кодcircular_buffer/debug.hpp.
![[Caution]](/img/caution.png) |
Caution |
Поскольку код отладки делает<circular_buffer >и его итераторы более взаимосвязанными, гарантии безопасности потоков<circular_buffer >отличаются при включенной поддержке отладки. В дополнение к самому контейнеру все итераторы, отслеживаемые контейнером (включая любые его копии), должны быть защищены от одновременного доступа. В частности, это включает в себя копирование, уничтожение или получение итераторов из контейнера, даже если для доступа только для чтения. |
Поддержка отладки отключена по умолчанию. Чтобы включить его, необходимо определить<BOOST_CB_ENABLE_DEBUG
>макрос со значением 1 при компиляции кода с использованием<circular_buffer
>.
<circular_buffer
>совместим с библиотекойBoost.Interprocess, используемой для межпроцессной связи. Учитывая, что поддержка отладки circular_buffer основана на «сырых» указателях (что не допускается библиотекой Interprocess), код должен быть составлен с отключенной поддержкой отладки (т.е. с макросом<BOOST_CB_ENABLE_DEBUG
>, не определенным или не определенным до 0). Если этого не сделать, компиляция потерпит неудачу.