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

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

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

Вот мой код C++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_

1 ответ

Решение

директива

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

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

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

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

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

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

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
    // ...
};
Другие вопросы по тегам