С++ Move Semantics - Объединение API Legacy C
Я работаю с устаревшим API C, в котором приобретение какого-то ресурса дорого и освобождает этот ресурс абсолютно критично. Я использую С++ 14, и я хочу создать класс для управления этими ресурсами. Вот основной скелет вещи...
class Thing
{
private:
void* _legacy;
public:
void Operation1(...);
int Operation2(...);
string Operation3(...);
private:
Thing(void* legacy) :
_legacy(legacy)
{
}
};
На самом деле это не одноэлементный шаблон. Ничто не является статичным, и может быть много экземпляров Thing
, все управляющие своими собственными устаревшими ресурсами. Кроме того, это не просто умный указатель. Оболоченный указатель _legacy
является закрытым, и все операции отображаются через некоторые функции публичного экземпляра, которые скрывают унаследованный API от потребителей.
Конструктор является закрытым, потому что экземпляры Thing
будут возвращены из статического factory или named-constructor, который фактически приобретет ресурс. Вот дешевая имитация этого factory, используя malloc()
в качестве места для кода, который будет вызывать устаревший API...
public:
static Thing Acquire()
{
// Do many things to acquire the thing via the legacy API
void* legacy = malloc(16);
// Return a constructed thing
return Thing(legacy);
}
Вот деструктор, который отвечает за освобождение устаревшего ресурса, снова free()
является просто заполнителем...
~Thing() noexcept
{
if (nullptr != _legacy)
{
// Do many things to free the thing via the legacy API
// (BUT do not throw any exceptions!)
free(_legacy);
_legacy = nullptr;
}
}
Теперь я хочу убедиться, что ровно один старый ресурс управляется одним экземпляром Thing
. Я не хотел, чтобы пользователи класса Thing
проходили экземпляры по желанию - они должны либо принадлежать локально к классу или функции, либо напрямую, либо через unique_ptr
, либо завернуты с помощью shared_ptr
, которые могут быть переданы, С этой целью я удалил оператор присваивания и конструкторы копирования...
private:
Thing(Thing const&) = delete;
void operator=(Thing const&) = delete;
Однако это добавило дополнительную проблему. Либо мне пришлось изменить мой метод factory, чтобы вернуть unique_ptr<Thing>
или shared_ptr<Thing>
, или мне пришлось реализовать семантику перемещения. Я не хотел диктовать шаблон, под которым следует использовать Thing
, поэтому я решил добавить оператор move-move и move-assign-operator следующим образом:
Thing(Thing&& old) noexcept : _legacy(old._legacy)
{
// Reset the old thing state to reflect the move
old._legacy = nullptr;
}
Thing& operator= (Thing&& old) noexcept
{
if (&old != this)
{
swap(_legacy, old._legacy);
}
return (*this);
}
С этим все сделано, я мог бы использовать Thing
как локальный и переместить его...
Thing one = Thing::Acquire();
Thing two = move(one);
Я не мог сломать шаблон, пытаясь выполнить самоопределение:
Thing one = Thing::Acquire();
one = one; // Build error!
Я мог бы сделать unique_ptr
для одного...
auto three = make_unique<Thing>(Thing::Acquire());
Или a shared_ptr
...
auto three = make_shared<Thing>(Thing::Acquire());
Все работало так, как я ожидал, и мой деструктор работал в нужный момент во всех моих тестах. Фактически, единственное раздражение заключалось в том, что make_unique
и make_shared
оба фактически вызывали конструктор move - он не был оптимизирован, как я надеялся.
Первый вопрос: Я правильно выполнил оператор move-constructor и move-assign-operator? (Они довольно новы для меня, и это будет первый раз, когда я использовал его в гневе.)
Второй вопрос: Прокомментируйте этот шаблон! Это хороший способ обернуть устаревшие ресурсы в классе С++ 14?
Наконец: Должен ли я что-либо изменить, чтобы код был лучше, быстрее, проще или читабельнее?
Ответы
Ответ 1
struct free_thing{
void operator()(void* p)const{
// stuff
free(p);
}
};
using upthing=std::unique_ptr<void,free_thing>;
upthing make_thing(stuff){
void*retval;
// code
return upthing(retval);
}
Сохраните upthing
в Thing
как _legacy
. Используйте dtor по умолчанию, переместите ctor, переместите назначение для Thing
(=default
).
Код destroy находится в free_thing
.
Ваш ctor создает вверх.
Теперь пользователи могут обрабатывать ваш Thing
как тип значения только для перемещения.
Не записывайте собственных указателей, если вам это действительно нужно. Уникальные и общие делают для вас много: если вы пишете свой собственный умный указатель, используйте их как внутренние кишки.
Ответ 2
Вы должны поместить ваш Thing
в интеллектуальный указатель, тогда вам не нужно будет беспокоиться о семантике копирования и перемещении.
class Thing
{
private:
void* _legacy;
public:
void Operation1(...);
int Operation2(...);
string Operation3(...);
Thing(const Thing&) = delete;
Thing(Thing&&) = delete;
Thing& operator=(const Thing&) = delete;
Thing& operator=(Thing&&) = delete;
static std::shared_ptr<Thing> acquire() {
return std::make_shared<Thing>();
}
private:
Thing() : _legacy(malloc(16)) {
// ...
}
~Thing() {
free(_legacy);
}
};
Аналогично, вы можете сделать это с помощью unique_ptr
:
std::unique_ptr<Thing> acquire() {
return std::make_unique<Thing>();
}
Вы, казалось, подразумевали, что хотите иметь только один экземпляр этой вещи, хотя даже в вашем решении вы не пытались сделать что-либо подобное. Для этого вам нужны статические переменные. Хотя помните, что в этом случае ваш ресурс будет освобожден только после выключения функции main()
. Например:
static std::shared_ptr<Thing> acquire() {
static std::shared_ptr<Thing> instance;
if (!instance) {
instance = std::make_shared<Thing>();
}
return instance;
}
Или версия unique_ptr
:
static Thing& acquire() {
static std::unique_ptr<Thing> instance;
if (!instance) {
instance = std::make_unique<Thing>();
}
return *instance;
}
Или вы можете использовать weak_ptr
для получения одного экземпляра программы, который освобождается, когда никто его не использует. В этом случае вы не сможете использовать unique_ptr
для этой цели. Эта версия будет воссоздавать объект, если он был освобожден, а затем снова нужен.
static std::shared_ptr<Thing> acquire() {
static std::weak_ptr<Thing> instance;
if (instance.expired()) {
instance = std::make_shared<Thing>();
}
return instance.lock();
}