智能指针与QVector组合使用问题

智能指针与QVector组合使用问题

引言

在C++开发中,由于Qt库接口完善易用,很多桌面端程序会使用Qt各种容器库[QTL]来代替STL,关于使用QTL还是STL,萝卜白菜,各有所爱,没有绝对优劣之分;

但是可能由于Qt库成型较早缘故,其未能考虑到现代C++的演化,当Qt库与现代C++标准库组合使用时,有时会产生一些意想不到的问题;

问题由来

开发中,常常需要将对象指针序列放存放在 vector 中,比如Qt库中的QVector,且为了自动内存管理,会使用智能指针 unique_ptr 来自动管理内存 [使用 unique_ptr 而不是 shared_ptr 的原因是为了更好的区分所有权];

为了阐明问题实质,将实际开发中的复杂问题简化为下述简单示例,但是麻雀虽小,五脏俱全,问题关键在下述代码已经体现:

#include <vector>
#include <unique_ptr>
#include <QVector>

using std::vector;
using std::unique_ptr;

class CTest {
    // ...
};

int main()
{
    unique_ptr<CTest> ptr = std::make_unique<CTest>();
    
    QVector<unique_ptr<CTest>> ptrVec;  // 不能通过编译
    // vector<unique_ptr<CTest>> ptrVec;  // 可以通过编译
    
    ptrVec.push_back(std:move(ptr));
    
}

上述代码中,使用 QVector 时并不能通过编译,若将 QVector 替换成 std::vector,便能通过编译,正常运行;

原因分析

当使用 QVector 时,编译器在 QVector 的 reallocData 函数中报错,详见下述代码与注释 [ 代码片段来自 Qt5.12.10 中的 qvector.h ] :

template <typename T>
void QVector<T>::reallocData(const int asize, const int aalloc, QArrayData::AllocationOptions options)
{
    // ...
    
    // QVector 隐式共享机制导致 unique_ptr 不能使用;
    // 下述代码中 isShared 值为 true , 可以通过对本部分代码的调试来验证这一点;
    if (isShared || !std::is_nothrow_move_constructible<T>::value) {
        // we can not move the data, we need to copy construct it
        while (srcBegin != srcEnd)
            new (dst++) T(*srcBegin++);    // 主要是本行代码需要使用 复制构造 产生问题
    } else {
        while (srcBegin != srcEnd)
            new (dst++) T(std::move(*srcBegin++));
    }
    
    // ...
}

通过上述代码的行为可知,由于 QVector 使用隐式共享[或称写时复制(COW : copy-on-write)]机制,reallocData 函数中的 isShared 值为 true,所以需要进行复制构造操作,而 std::unique_ptr 复制构造函数无法调用,所以编译器报错,不能通过编译;

std::unique_ptr 相关部分代码如下 [ 代码片段来自 mingw7.3.0 中的 unique_ptr.h ] :

template <typename _Tp, typename _Dp = default_delete<_Tp>>
class unique_ptr {
    // ...

    // Disable copy from lvalue.
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
    
    // ...
};

上述问题也是所有问题中我们最希望出现的一类,因为能在编译期发现问题,这比在运行时出现意想不到的错误要好很多,这也是编译型语言的优势所在;

最佳实践

上述问题的根源是 QVector 内部设计所致:QVector 使用隐式共享机制,其在reallocData 等函数实现中需要使用类的复制构造, 因此 QVector 不适合存放复制构造函数无法调用的类,具体包含下述两种情况:

  • 复制构造函数 =delete;
  • 复制构造函数为 private;

示例代码如下:

// 不能使用 QVector<T> 的情况1 : 赋值构造函数 被删除
class Demo1 {
public:
    Demo1() = default;
    Demo1(const Demo1& rhs) = delete;
    Demo1& operator=(const Demo1& rhs) = delete;
    Demo1(Demo1&& rhs) = default;
    Demo1& operator=(Demo1&& rhs) = default;
    ~Demo1() = default;
    
    // ...   
private:
    Obj m_obj;
};


// 不能使用 QVector<T> 的情况2 : 赋值构造函数 私有化
class Demo2 {
public:
    Demo2() = default;
    Demo2(Demo2&& rhs) = default;
    Demo2& operator=(Demo2&& rhs) = default;
    ~Demo2() = default;

private:
    Demo2(const Demo2& rhs);
    Demo2& operator=(const Demo2& rhs);
    
    // ...   
private:
    Obj m_obj;
};

Qt 中 QVector 以及 QString 等库的设计中采用了隐式共享的技术以提高性能,但是有些情况下会导致类似上述问题,而 GCC 等主流编译器中标准库对 vector 与 string 等的实现并不采用此技术,故不会产生以上问题,Qt 库不知后期会不会改进设计,以更好的兼容标准库;

后记

后来搜索了一下,发现在 StackOverflow 中也有类似的问题:

Use std::find on QVector<std::unique_ptr>

其中回答也给出了简要解释,若要更为详细的介绍,可阅读本文;


本文作者: 王同学