В C++ можно, как и в C, выделить себе участок памяти, в котором будут жить объекты (в широком смысле, в том числе встроенного типа, напр.
int
). Чтобы начать жизнь объекта, его
нужно разместить в памяти с соответствующим типу выравниванием. Если выравнивание, равное степени двойки, будет меньше требуемого для типа, то приведение с помощью
static_cast
типа указателя в выделенной памяти (
void*, char*, unsigned char*
) к указателю на тип объекта
создает неопределенное поведение. Значит, чтобы безопасно использовать объект, размещенный в выделенной памяти, нужно убедиться, что он размещен (с помощью placement new) по выровненному адресу.
Пусть мы создаем основной буфер под объект вызовом
new unsigned char[size]
. Будет ли буфер иметь достаточное выравнивание, чтобы в начале его разместить
BUFFER::a
типа
int
? -
Да, если мы зарезервируем сразу достаточно памяти,
In addition, if the new-expression is used to allocate an array of char, unsigned char, or std::byte (since C++17), it may request additional memory from the allocation function if necessary to guarantee correct alignment of objects of all types no larger than the requested array size, if one is later placed into the allocated array.
Выражения
new, new[]
автоматически определяют размер выделяемой памяти и передают его низкоуровневым функциям
operator new, operator new[]
соответственно. Память, выделенная
new
, освобождается вызовом
delete
, для
new[]
-
delete[]
. Поведение
delete
похоже на поведение
new
выше.
Значит, у нас есть всё необходимое чтобы реализовать свой класс буфера.
Например,
#include <cstddef>
#include <iostream>
#include <cstring>
class Buffer {
public:
using preamble_type = int;
static constexpr std::size_t preamble_size = sizeof(preamble_type);
Buffer(const preamble_type& value, std::size_t bufferSize) {
buffer_ = new unsigned char[preamble_size + bufferSize];
::new(buffer_) preamble_type(value);
}
Buffer(const Buffer& other) = delete;
Buffer& operator=(const Buffer& rhs) = delete;
Buffer(Buffer&& other) noexcept
: buffer_(other.buffer_) {
other.buffer_ = nullptr;
}
Buffer& operator=(Buffer&& rhs) noexcept {
std::swap(buffer_, rhs.buffer_);
return *this;
}
virtual ~Buffer() {
destroy();
}
preamble_type& preamble() noexcept {
return const_cast<preamble_type&>(
const_cast<const Buffer *>(this)->preamble()
);
}
const preamble_type& preamble() const noexcept {
return *preambleData();
}
unsigned char* data() const noexcept {
return buffer_ + preamble_size;
}
void resize(std::size_t size) {
if (buffer_ != nullptr) {
auto newBuffer = new unsigned char[preamble_size + size];
::new(newBuffer) preamble_type(std::move(preamble()));
destroy();
buffer_ = newBuffer;
}
}
private:
preamble_type* preambleData() const noexcept {
// return std::launder(reinterpret_cast<preamble_type*>(buffer_)); c++17
return reinterpret_cast<preamble_type*>(buffer_);
}
void destroy() noexcept {
preambleData()->~preamble_type();
delete[] buffer_;
}
unsigned char* buffer_ = nullptr;
};
int main()
{
using std::cout;
using std::endl;
const std::size_t bufferSize = 100;
Buffer b(100, bufferSize);
const char hello[] = "hello world!";
memcpy(b.data(), hello, sizeof(hello));
auto c = std::move(b);
cout << c.preamble() << ' ' << c.data() << endl;
b = Buffer(5, 20);
memcpy(b.data(), hello, sizeof(hello));
cout << b.preamble() << ' ' << b.data() << endl;
return 0;
}
Из кода можно выкинуть всё, что касается
preamble_type
, если считать, что первое поле всегда будет
int
. С другой стороны, код можно сделать обобщенным с минимальными изменениями. Например,
template<typename PreambleT>
class Buffer {
public:
using preamble_type = PreambleT;
//...
};
Такой пример
class Buffer
, по-моему, скорее плохой, потому что только имитирует на C++ код, написанный на C. Так, мы не храним в классе размер буфера, поэтому нельзя определить операторы копирования. Детали реализации класса "утекают" в пользовательский код.
Пока мы явно не сформулировали и не поддерживаем
инварианты класса. Какое состояние объекта мы можем считать пригодным для использования?
В частности, мы можем переместить содержимое буфера в другой объект конструктором перемещения, но потом к исходному буферу не можем применить
resize
. Даже если бы
resize
давал нам новый буфер, значение
preamble
потеряно. Если бы мы сохраняли
preamble
, тогда в конструкторе перемещения пришлось бы выделять память под новый буфер, но тогда этот конструктор уже не будет
noexcept
- плохо. Придется запретить перемещение?
Если нужно убрать из класса всю работу с памятью, можно реализовать свой аллокатор, который будет размещать данные линейно, или поискать готовую реализацию.
Наверняка есть лучшее проектное решение, такое которое не будет имитировать реализацию на C. Возможно, имеет смысл портировать решение на уровне интерфейсов, переписывая полностью на C++ детали реализации.