美文网首页
面试官的动机——实现智能指针1:unique_ptr

面试官的动机——实现智能指针1:unique_ptr

作者: 丹丘生___ | 来源:发表于2018-10-14 17:23 被阅读0次

    要求面试者实现一个简单的unique_ptr,能够考察其对许多知识点的掌握,尤其是《C++ Primer》第五版12~16章中介绍的内容。C++初学者学习完这些内容后,应该动手实现一遍unique_ptrshared_ptr。下面列出需要注意的知识点:

    1. 异常
    2. unique_ptr本身功能
    3. 三五法则和阻止拷贝
    4. 隐式的类类型转换
    5. 移动构造、移动赋值及自赋值问题
    6. 编写模板类。

    除以上几点外,还有若干细节需要注意。接下来的代码将逐步呈现它们。


    初始版本.

    首先应该查看unique_ptr 的接口文档,然后,实现其一部分构造函数和其他成员。然后对照上面列出的几点,找出下面实现存在的问题。

    template<typename T>
    class MyUniquePtr
    {
    public:
        MyUniquePtr(T* ptr = nullptr)
            :mPtr(ptr)
        {}
    
        ~MyUniquePtr()
        {
            if(mPtr)
                delete mPtr;
        }
    
        MyUniquePtr(MyUniquePtr &&p) ;
        MyUniquePtr& operator=(MyUniquePtr &&p) ;
    
        MyUniquePtr(const MyUniquePtr &p) = delete;
        MyUniquePtr& operator=(const MyUniquePtr &p) = delete;
    
        T* operator*() const {return mPtr;}
        T& operator->()const {return *mPtr;}
    
        void reset(T* q = nullptr)
        {
            if(q != mPtr){
                if(mPtr)
                    delete mPtr;
                mPtr = q;
            }
        }
    
        T* release()
        {
            T* res = mPtr;
            mPtr = nullptr;
            return res;
        }
        T* get() const {return mPtr;}
        void swap(MyUniquePtr &p)
        {
            using std::swap;
            swap(mPtr, p.mPtr);
        }
    private:
        T* mPtr;
    };
    
    template<typename T>
    MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p)
    {
        if(*this != p)
        {
            if(mPtr)
                delete mPtr;
            mPtr = p.mPtr;
            p.mPtr = NULL;
        }
        return *this;
    }
    
    template<typename T>
    MyUniquePtr<T> :: MyUniquePtr(MyUniquePtr &&p) : mPtr(p.mPtr)
    {
        p.mPtr == NULL;
    }
    

    问题1:异常——缺失noexcept关键字

    上述代码中的移动构造函数、移动赋值操作符以及部分接口函数的声明和定义中,缺失了noexcept关键字,该关键字能够告知标准库,本函数不会产生异常,如果未标记noexcept,标准库将产生一个额外的操作,增加了开销。

    问题2:缺失bool值转换功能

    观察IO类、unique_ptr、shared_ptr等资源管理相关的类,它们一般都会重新实现:

    explicit operation bool() const noexcept

    以便能直接将对象放置于条件语句中进行判断。

    int main()
    {
        std::unique_ptr<int> ptr(new int(42));
    
        if (ptr) std::cout << "before reset, ptr is: " << *ptr << '\n';
        ptr.reset();
        if (ptr) std::cout<< "after reset, ptr is: " << *ptr << '\n';
    }
    

    explicit关键字不能少,否则可能出现隐式转换,导致如下问题:

    std::unique_ptr<int> p1(new int(13));
    std::unique_ptr<int> p2(new int(14));
    
    if(p1 == p2)
    {
        //p1 p2 都会被转换为bool值,都为true,因此结果是两者相等。
        std::cout << "p1 is equal p2" << endl;
    }
    
    //输出: p1 is equal p2
    

    这时p1==p2将返回true
    编译器为了使两个对象能够互相比较,就会把他们都转换为bool型,从而导致错误。


    问题3:三/五法则和阻止拷贝

    释放动态内存---->需要自定义析构函数,而根据三/五法则:

    如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝构造函数和拷贝赋值运算符。

    总之,不能让编译器产生合成版本的拷贝构造函数和拷贝赋值运算符。
    而unique_ptr要求独占资源,不允许拷贝,因此,上述代码中将拷贝赋值操作删除(通过delete关键字),就不会产生合成版本的了。


    问题4:隐式的类类型转换

    如果构造函数只接受一个实参,则它实际上定义了转换此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数(converting constructor)

    是否允许隐式的类类型转换,是由类的语义和上下文环境决定的。
    同样的,使用explicit可以抑制这种转换。也由此,unique_ptr只能进行直接初始化。


    5.移动构造、移动赋值及自赋值问题

    unique_ptr不可拷贝,但是可以移动。这是它的重要特征,因此,绝对不能忘记自定义移动构造函数和移动赋值运算符。
    而重新实现移动赋值运算符时,要记得考虑自赋值问题。上述代码考虑了该问题。

    优化:使用swap函数来实现operator=(移动赋值运算符),巧妙解决自赋值问题。且相比判断*this == p而言,提高了效率。

    template<typename T>
    MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p) noexcept
    {
    //交换本对象和右值p的内容
        this->swap(*this, p);//p.mPtr现在指向本对象曾经指向的内存
        return *this; //返回后,右值p将被销毁,从而指向delete p.mPtr
    }
    

    以下英文内容摘自Copy-and-Swap Idiom in C++

    为什么*this == p版本不好:

    1.自赋值检查: 自赋值情况的发生频率并不高,而无论何种情况都做自赋值检查,更加耗时。

    为什么swap版本更好:

    1. 无需再做自赋值检查。由于自赋值检查发生频率很低,因此,所有在自赋值情况下复制相等的指针,其所浪费的时间并不多。
    1. 代码复用。复用了swap函数,精简了代码.
    1. 使用交换技术,能够延后针对unique_ptr内置指针成员的delete操作。这意味着临时的unique_ptr超出作用域之前,是潜在的可再次使用的。当其超出作用域后,unique_ptr的析构函数会将其正确销毁。

    6.编写模板类

    接下来的考察点,就是编写模板类了。没有太多可赘述的。


    修改后的版本:

    template<typename T>
    class MyUniquePtr
    {
    public:
       explicit MyUniquePtr(T* ptr = nullptr)
            :mPtr(ptr)
        {}
    
        ~MyUniquePtr()
        {
            if(mPtr)
                delete mPtr;
        }
    
        MyUniquePtr(MyUniquePtr &&p) noexcept;
        MyUniquePtr& operator=(MyUniquePtr &&p) noexcept;
    
        MyUniquePtr(const MyUniquePtr &p) = delete;
        MyUniquePtr& operator=(const MyUniquePtr &p) = delete;
    
        T* operator*() const noexcept {return mPtr;}
        T& operator->()const noexcept {return *mPtr;}
        explicit operator bool() const noexcept{return mPtr;}
    
        void reset(T* q = nullptr) noexcept
        {
            if(q != mPtr){
                if(mPtr)
                    delete mPtr;
                mPtr = q;
            }
        }
    
        T* release() noexcept
        {
            T* res = mPtr;
            mPtr = nullptr;
            return res;
        }
        T* get() const noexcept {return mPtr;}
        void swap(MyUniquePtr &p) noexcept
        {
            using std::swap;
            swap(mPtr, p.mPtr);
        }
    private:
        T* mPtr;
    };
    
    template<typename T>
    MyUniquePtr<T>& MyUniquePtr<T>::operator=(MyUniquePtr &&p) noexcept
    {
        swap(*this, p);
        return *this;
    }
    
    template<typename T>
    MyUniquePtr<T> :: MyUniquePtr(MyUniquePtr &&p) noexcept : mPtr(p.mPtr)
    {
        p.mPtr == NULL;
    }
    

    相信在面试中,写一个涵盖以上知识点的unique_ptr应该足够了。
    欢迎指出代码中的问题。

    相关文章

      网友评论

          本文标题:面试官的动机——实现智能指针1:unique_ptr

          本文链接:https://www.haomeiwen.com/subject/mwyzaftx.html