【问题标题】:Should I delete the move constructor and the move assignment of a smart pointer?我应该删除移动构造函数和智能指针的移动分配吗?
【发布时间】:2016-09-02 17:40:14
【问题描述】:

我正在实现一个简单的智能指针,它基本上跟踪对它处理的指针的引用数。

我知道我可以实现移动语义,但我认为这没有意义,因为复制智能指针非常便宜。特别是考虑到它带来了产生讨厌的错误的机会。

这是我的 C++11 代码(我省略了一些无关紧要的代码)。也欢迎普通的 cmets。

#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_

【问题讨论】:

  • 没有。如果您希望所有内容都是副本,则不要声明移动成员;不要将它们定义为已删除。 stackoverflow.com/questions/26489837/…
  • 您对operator* 的看法完全正确!哎呀!已更正。
  • std::shared_ptr 具有自定义移动操作,因为底层引用计数机制是并发安全的。因此,自定义移动操作可以避免原子增量。
  • 好吧,如果你想知道一些好的方法,那就锁定std::shared_ptr代码吧。
  • “复制一个智能指针很便宜”,但移动一个必须更便宜 - 不需要任何原子或锁定,因为我们没有触及计数?

标签: c++ c++11 smart-pointers move-semantics


【解决方案1】:

指南

永远不要删除必杀技成员。

在典型代码中(例如在您的问题中),删除移动成员有两个动机。其中一个动机会产生不正确的代码(如您的示例),而另一个动机是删除移动成员是多余的(既无害也无好处)。

  1. 如果您有一个可复制的类并且您不想移动成员,只需不要声明它们(包括不删除它们)。仍会声明已删除的成员。删除的成员参与重载决议。不在场的成员没有。当您使用有效的复制构造函数和已删除的移动成员创建类时,您不能从函数中按值返回它,因为重载决议将绑定到已删除的移动成员。

  2. 有时人们想说:这个类既不能移动也不能复制。删除副本和移动成员是正确的。但是,只需删除复制成员就足够了(只要未声明移动成员)。声明(甚至删除)复制成员禁止编译器声明移动成员。所以在这种情况下,被删除的 move 成员只是多余的。

如果你声明删除的 move 成员,即使你碰巧选择了它是多余的而不是不正确的情况,每次有人阅读你的代码时,他们都需要重新发现你的情况是多余的还是不正确的。让您的代码阅读者更轻松,并且永远不要删除移动成员。

不正确的情况:

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

这是您可能希望使用 CopyableButNotMovble 但在编译时会失败的示例代码:

#include <algorithm>
#include <vector>

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

    CopyableButNotMovble(int);
    // ...
    friend bool operator<(CopyableButNotMovble const& x, CopyableButNotMovble const& y); 
};

int
main()
{
    std::vector<CopyableButNotMovble> v{3, 2, 1};
    std::sort(v.begin(), v.end());
}

In file included from test.cpp:1:
algorithm:3932:17: error: no
      matching function for call to 'swap'
                swap(*__first, *__last);
                ^~~~
algorithm:4117:5: note: in
      instantiation of function template specialization 'std::__1::__sort<std::__1::__less<CopyableButNotMovble,
      CopyableButNotMovble> &, CopyableButNotMovble *>' requested here
    __sort<_Comp_ref>(__first, __last, __comp);
    ^
algorithm:4126:12: note: in
      instantiation of function template specialization 'std::__1::sort<CopyableButNotMovble *,
      std::__1::__less<CopyableButNotMovble, CopyableButNotMovble> >' requested here
    _VSTD::sort(__first, __last, __less<typename iterator_traits<_RandomAccessIterator>::value_type>());
           ^
...

(来自 std::lib 深处的许多讨厌的错误消息)

正确的做法是:

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
    // ...
};

Here is a more in-depth explanation of this declaration style.

【讨论】:

  • 我没有看到“产生不正确的代码(如您的示例)”。代码中有什么不正确的地方?我是否忽略了这里的具体解释?你指的是“=delete 并不意味着“不要使用我,而是使用下一个最好的”。它的意思是,“当你需要我的时候不要使用我——而是在野外独自一人。”来自@ 987654322@ 还是别的什么?
  • @towi:我添加了一个示例来澄清。感谢您指出这一点。
  • @HowardHinnant “当您创建具有有效复制构造函数和已删除移动成员的类时,您不能从函数中按值返回它,因为重载决议将绑定到已删除移动成员。”我对此一无所知。谢谢!
  • 由于 C++17 保证返回时复制省略,底层机制发生了变化,因此 1 不再适用。在 2 的情况下。我现在建议只删除移动赋值运算符,因为这是摆脱复制和移动的最少操作,同时保留默认的默认构造函数。
  • @HowardHinnant:我同意你的回答,尽管编译器定义默认操作时需要一些关于确切规则的知识(无论如何,这应该是一个优秀的 C++ 程序员所期望的)。一个问题:您的回答似乎与this C++ FAQ entry 相矛盾——作为委员会成员,您认为有必要更新吗?
猜你喜欢
  • 2011-10-04
  • 2016-07-07
  • 1970-01-01
  • 2013-08-08
  • 1970-01-01
  • 2015-06-07
  • 1970-01-01
  • 2015-08-04
  • 2018-03-26
相关资源
最近更新 更多