С++ 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();
}