Подтвердить что ты не робот

Должен ли я удалить конструктор перемещения и назначение перемещения умного указателя?

Я реализую простой умный указатель, который в основном отслеживает количество ссылок на указатель, который он обрабатывает.

Я знаю, что могу реализовать семантику перемещения, но я не думаю, что это имеет смысл, поскольку копирование умного указателя очень дешево. Особенно учитывая, что он вводит возможности для создания неприятных ошибок.

Вот мой код С++ 11 (я пропустил какой-то несущественный код). Общие комментарии также приветствуются.

#ifndef SMART_PTR_H_
#define SMART_PTR_H_

#include <cstdint>

template<typename T>
class SmartPtr {
private:
    struct Ptr {
        T* p_;
        uint64_t count_;
        Ptr(T* p) : p_{p}, count_{1} {}
        ~Ptr() { delete p_; }
    };
public:
    SmartPtr(T* p) : ptr_{new Ptr{p}} {}
    ~SmartPtr();

    SmartPtr(const SmartPtr<T>& rhs);
    SmartPtr(SmartPtr<T>&& rhs) =delete;

    SmartPtr<T>& operator=(const SmartPtr<T>& rhs);
    SmartPtr<T>& operator=(SmartPtr<T>&& rhs) =delete;

    T& operator*() { return *ptr_->p_; }
    T* operator->() { return ptr_->p_; }

    uint64_t Count() const { return ptr_->count_; }

    const T* Raw() const { return ptr_->p_; }
private:
    Ptr* ptr_;
};

template<typename T>
SmartPtr<T>::~SmartPtr() {
    if (!--ptr_->count_) {
        delete ptr_;
    }
    ptr_ = nullptr;
}

template<typename T>
SmartPtr<T>::SmartPtr(const SmartPtr<T>& rhs) : ptr_{rhs.ptr_} {
    ++ptr_->count_;
}

template<typename T>
SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T>& rhs) {
    if (this != &rhs) {
        if (!--ptr_->count_) {
            delete ptr_;
        }
        ptr_ = rhs.ptr_;
        ++ptr_->count_;
    }
    return *this;
}

#endif // SMART_PTR_H_
4b9b3361

Ответ 1

Руководство

Никогда не удаляйте специальные элементы перемещения.

В типичном коде (например, в вашем вопросе) есть две причины для удаления участников перемещения. Одна из этих мотивов порождает неверный код (как в вашем примере), а для другой мотивации удаление элементов перемещения является избыточным (не наносит никакого вреда и не хорош).

  • Если у вас есть класс для копирования и вы не хотите перемещать элементы, просто не объявляйте их (в том числе не удаляя их). Удаленные участники все еще объявлены. Удаленные участники участвуют в разрешении перегрузки. Члены, которых нет, не делают. Когда вы создаете класс с допустимым конструктором копирования и удаленным элементом перемещения, вы не можете вернуть его по значению из функции, потому что разрешение перегрузки будет связано с удаленным элементом перемещения.

  • Иногда люди хотят сказать: этот класс не движется и не копируется. Правильно удалить и копию, и элементы перемещения. Однако достаточно просто удалить элементы копии (пока элементы перемещения не объявлены). Объявленные (даже удаленные) члены-копии запрещают компилятору объявлять элементы перемещения. Поэтому в этом случае элементы удаленного перемещения просто избыточны.

Если вы объявляете удаленные элементы перемещения, даже если вам удастся выбрать случай, когда он является избыточным и не является неправильным, каждый раз, когда кто-то читает ваш код, им нужно вновь обнаружить, является ли ваш случай избыточным или неправильным. Сделайте это проще для читателей вашего кода и никогда не удаляйте элементы перемещения.

Неверный случай:

struct CopyableButNotMovble
{
    // ...
    CopyableButNotMovble(const CopyableButNotMovble&);
    CopyableButNotMovble& operator=(const CopyableButNotMovble&);
    CopyableButNotMovble(CopyableButNotMovble&&) = delete;
    CopyableButNotMovble& operator=(CopyableButNotMovble&&) = delete;
    // ...
};

Вот пример кода, который вы, вероятно, ожидаете работать с CopyableButNotMovble, но не будет работать во время компиляции:

CopyableButNotMovble
get()
{
    CopyableButNotMovble x;
    return x;
}

int
main()
{
    CopyableButNotMovble x = get();
}

test.cpp:22:26: error: call to deleted constructor of 'CopyableButNotMovble'
    CopyableButNotMovble x = get();
                         ^   ~~~~~
test.cpp:7:5: note: 'CopyableButNotMovble' has been explicitly marked deleted here
    CopyableButNotMovble(CopyableButNotMovble&&) = delete;
    ^
1 error generated.

Правильный способ сделать это:

struct CopyableButNotMovble
{
    // ...
    CopyableButNotMovble(const CopyableButNotMovble&);
    CopyableButNotMovble& operator=(const CopyableButNotMovble&);
    // ...
};

Резервный случай:

struct NeitherCopyableNorMovble
{
    // ...
    NeitherCopyableNorMovble(const NeitherCopyableNorMovble&) = delete;
    NeitherCopyableNorMovble& operator=(const NeitherCopyableNorMovble&) = delete;
    NeitherCopyableNorMovble(NeitherCopyableNorMovble&&) = delete;
    NeitherCopyableNorMovble& operator=(NeitherCopyableNorMovble&&) = delete;
    // ...
};

Более читаемый способ сделать это:

struct NeitherCopyableNorMovble
{
    // ...
    NeitherCopyableNorMovble(const NeitherCopyableNorMovble&) = delete;
    NeitherCopyableNorMovble& operator=(const NeitherCopyableNorMovble&) = delete;
    // ...
};

Это помогает, если вы практикуете группировку всех 6 ваших специальных членов в верхней части объявления класса, в том же порядке, пропуская те, которые вы не хотите объявлять. Эта практика облегчает читателям вашего кода быстрое определение того, что вы специально не объявили какого-либо особого участника.

Например, вот следующий шаблон:

class X
{
    // data members:

public:
    // special members
    ~X();
    X();
    X(const X&);
    X& operator=(const X&);
    X(X&&);
    X& operator=(X&&);

    // Constructors
    // ...
};