1. 简介
在Qt/C++ 借助QVariant实现可存储通用类型的容器这篇文章中,我们介绍了如何使用QVariant
来实现一个可存储通用数据类型的容器。其中,最重要的就是QVariant
类。它的作用类似于Qt数据类型的联合,它还能支持存储用户自定义的类型。
文章中提到,toT()
(例如toInt()
、toString()
)是const
方法(value()
也是如此),这意味着通过这些方法获取的数据实际上是QVariant
内部数据的副本,即外部对获取得到的数据的修改并不会影响其内部的数据。但是,在实际开发中,我们常常希望获取的是QVariant
内部数据本身,尤其是当QVariant
内存储的是不支持隐式共享的大批量数据时,因为这意味着这里会发生一次拷贝,影响程序性能。除了出于性能的考虑,我们可能还需要对内部的数据进行修改。也就是说,我们对QVariant
的内部数据的访问提出了如下两个需求:
- 零拷贝读取内部数据;
- 直接对内部数据完成读-改-写。
下面就介绍如何使用一些奇技淫巧来实现这两个需求。
2. 零拷贝读取内部数据
如果想要使用零拷贝的方式访问数据内容,我们首先能想到的就是使用指针。不得不说,Qt的设计者们真的是为我们预留了很大的自由度,我们确实可以通过指针直接访问QVariant
内存储的数据。QVariant
(Qt 5.15)提供了如下三个接口用于获取内部数据指针:
void *data();
const void *constData() const;
inline const void *data() const { return constData(); }
注意,官方文档对void *data()
的描述如下:
Returns a pointer to the contained object as a generic void* that can be written to.
This function detaches the QVariant. When called on a null-QVariant, the QVariant will not be null after the call.
也就是说,void *data()
会在内部调用detach()
,这意味着这里可能会导致一次数据拷贝(QVariant
处于共享状态时)。因此,如果仅仅是为了读取数据,建议选用constData()
。后面会通过实验验证。
既然数据指针都获取到了,要访问内容不就是轻而易举的事情嘛。为了让这件事情更加“泰裤辣”,我们可以使用如下的函数模板实现更加通用的转换:
template<typename T>
inline const T *castConstPtr(QVariant & var)
{
if (var.canConvert<T>()) {
return reinterpret_cast<const T *>(var.constData());
}
return nullptr;
}
template<typename T>
inline T *castPtr(QVariant & var)
{
if (var.canConvert<T>()) {
return reinterpret_cast<T *>(var.data());
}
return nullptr;
}
下面我们用实验来验证一下data()
与constData()
的效果以及它们的区别。我的验证环境如下:
- CPU: Intel® Core™ i7-10750H CPU @ 2.60GHz 2.59 GHz
- OS:Windows 11
- Compiler:MSVC 19.32.31332.
我们分别使用std::vector<uint8_t>
和QVector<uint8_t>
创建包含10亿个元素(每个元素的值设置为66)的实例,它们占用内存大小分别约等于953.74MB。我们先将数据放入QVariant
,然后分别使用上面的castConstPtr
和castPtr()
以及原生的value()
获取数据,并通过QElapsedTimer
统计各个操作的耗时。为了解释的便利,本文设计了两个实验。
2.1. 实验1:感受castConstPtr和castPtr的魅力
实验1的关键代码如下:
QElapsedTimer profiler;
QVariant var;
QVariant var1;
std::vector<uint8_t> vec(1000000000, 66);
QVector<uint8_t> vec1(1000000000, 66);
profiler.start();
var.setValue(vec);
auto consume1 = profiler.restart();
var1.setValue(vec1);
auto consume2 = profiler.restart();
const auto *constPtr = castConstPtr<std::vector<uint8_t>>(var);
auto consume3 = profiler.restart();
const auto *constPtr1 = castConstPtr<QVector<uint8_t>>(var1);
auto consume4 = profiler.restart();
const auto *ptr = castPtr<std::vector<uint8_t>>(var);
auto consume5 = profiler.restart();
const auto *ptr1 = castPtr<QVector<uint8_t>>(var1);
auto consume6 = profiler.restart();
auto value = var.value<std::vector<uint8_t>>();
auto consume7 = profiler.restart();
auto value1 = var1.value<QVector<uint8_t>>();
auto consume8 = profiler.restart();
if (constPtr != nullptr && constPtr1 != nullptr &&
ptr != nullptr && ptr1 != nullptr) {
qInfo() << "constPtr[0]" << constPtr->at(0);
qInfo() << "constPtr1[0]" << constPtr1->at(0);
qInfo() << "ptr[0]" << ptr->at(0);
qInfo() << "ptr1[0]" << ptr1->at(0);
qInfo() << "value[0]" << value.at(0);
qInfo() << "value1[0]" << value1.at(0);
}
else {
qCritical() << "cast error!";
}
qInfo() << "var detach" << var.isDetached();
qInfo() << "var1 detach" << var1.isDetached();
qInfo() << "[setValue] for var consume" << consume1 << "ms";
qInfo() << "[setValue] for var1 consume" << consume2 << "ms";
qInfo() << "[castConstPtr] for var consume" << consume3 << "ms";
qInfo() << "[castConstPtr] for var1 consume" << consume4 << "ms";
qInfo() << "[castPtr] for var consume" << consume5 << "ms";
qInfo() << "[castPtr] for var1 consume" << consume6 << "ms";
qInfo() << "[value] for var consume" << consume7 << "ms";
qInfo() << "[value] for var1 consume" << consume8 << "ms";
程序运行输出的结果如下:
constPtr[0] 66
constPtr1[0] 66
ptr[0] 66
ptr1[0] 66
value[0] 66
value1[0] 66
var detach true
var1 detach true
[setValue] for var consume 212 ms
[setValue] for var1 consume 0 ms
[castConstPtr] for var consume 0 ms
[castConstPtr] for var1 consume 0 ms
[castPtr] for var consume 0 ms
[castPtr] for var1 consume 0 ms
[value] for var consume 211 ms
[value] for var1 consume 0 ms
首先,根据前6行的输出,我们可以得知通过castConstPtr()
和castPtr()
得到的数据是正确的。
查看第7-8行的输出,可以看到var
和var1
都已经处于detached状态。
查看9-10行,我们发现,将QVector<uint8_t>
存入QVariant
中的速度比std::vector<uint8_t>
快太多了。这就是Qt隐式共享的威力所在,因为QVector
支持隐式拷贝,实际上拷贝到QVariant
仅仅是QVector
的元数据,而不是真实的数据内容。
查看11-14行可知,castConstPtr()
和castPtr()
的访问速度竟然是一样快的。因为在7-8行中,我们得知了var
和var1
都已经被detach()
,那是不是就意味着进行了拷贝呢?答案是否定的。下面就仔细看下这其中发生了什么:我们先查看isDetached()
的定义:
inline bool QVariant::isDetached() const
{ return !d.is_shared || d.data.shared->ref.loadRelaxed() == 1; }
通过简单的验证便可以知道,在执行data()
之前,var
的is_shared
是1且shared->ref.loadRelaxed()
为1;对于var1
,is_shared
是0,shared->ref.loadRelaxed()
是1000000000(原因未深究)。因此,它们在data()
之前就已经处于detached的状态了,故而在执行data()
时,两者都没有发生拷贝。其实这个结果也是符合常理的,因为var
和var1
各自都只有一个备份,此时不存在共享数据的情况,所以按常理来说也不应该发生拷贝。但是,如果var
或者var1
处于共享状态时,那结果就不一样了,详情参看实验2。
从最后的两行日志可以看出两点:
value()
返回数据时确实会发生一次拷贝;- Qt的隐式共享功能真的很香。因为对于这类数据结构而言,即使我们不使用本文提到的
castPtr()
和castConstPtr()
,也不需要花费多大的代价就完成了拷贝。
2.2. 实验2:谨慎使用data()读取数据
实验2与实验1的关键代码差不多,仅仅增加了对var
和var1
的复制。关键代码如下:
QElapsedTimer profiler;
QVariant var;
QVariant var1;
std::vector<uint8_t> vec(1000000000, 66);
QVector<uint8_t> vec1(1000000000, 66);
profiler.start();
var.setValue(vec);
auto consume1 = profiler.restart();
var1.setValue(vec1);
auto consume2 = profiler.restart();
QVariant varCopy(var);
auto consume3 = profiler.restart();
QVariant var1Copy(var1);
auto consume4 = profiler.restart();
const auto *constPtr = castConstPtr<std::vector<uint8_t>>(var);
auto consume5 = profiler.restart();
const auto *constPtr1 = castConstPtr<QVector<uint8_t>>(var1);
auto consume6 = profiler.restart();
const auto *ptr = castPtr<std::vector<uint8_t>>(var);
auto consume7 = profiler.restart();
const auto *ptr1 = castPtr<QVector<uint8_t>>(var1);
auto consume8 = profiler.restart();
auto value = var.value<std::vector<uint8_t>>();
auto consume9 = profiler.restart();
auto value1 = var1.value<QVector<uint8_t>>();
auto consume10 = profiler.restart();
if (constPtr != nullptr && constPtr1 != nullptr &&
ptr != nullptr && ptr1 != nullptr) {
qInfo() << "constPtr[0]" << constPtr->at(0);
qInfo() << "constPtr1[0]" << constPtr1->at(0);
qInfo() << "ptr[0]" << ptr->at(0);
qInfo() << "ptr1[0]" << ptr1->at(0);
qInfo() << "value[0]" << value.at(0);
qInfo() << "value1[0]" << value1.at(0);
}
else {
qCritical() << "cast error!";
}
qInfo() << "var detach" << var.isDetached();
qInfo() << "var1 detach" << var1.isDetached();
qInfo() << "[setValue] for var consume" << consume1 << "ms";
qInfo() << "[setValue] for var1 consume" << consume2 << "ms";
qInfo() << "copy for var consume" << consume3 << "ms";
qInfo() << "copy for var1 consume" << consume4 << "ms";
qInfo() << "[castConstPtr] for var consume" << consume5 << "ms";
qInfo() << "[castConstPtr] for var1 consume" << consume6 << "ms";
qInfo() << "[castPtr] for var consume" << consume7 << "ms";
qInfo() << "[castPtr] for var1 consume" << consume8 << "ms";
qInfo() << "[value] for var consume" << consume9 << "ms";
qInfo() << "[value] for var1 consume" << consume10 << "ms";
程序运行输出的结果如下:
constPtr[0] 66
constPtr1[0] 66
ptr[0] 66
ptr1[0] 66
value[0] 66
value1[0] 66
var detach true
var1 detach true
[setValue] for var consume 205 ms
[setValue] for var1 consume 0 ms
copy for var consume 0 ms
copy for var1 consume 0 ms
[castConstPtr] for var consume 0 ms
[castConstPtr] for var1 consume 0 ms
[castPtr] for var consume 212 ms
[castPtr] for var1 consume 0 ms
[value] for var consume 213 ms
[value] for var1 consume 0 ms
重点解释11-16行:
-
11-12行:对
QVariant
整体的拷贝并不耗时,因为Qt使用隐式共享的机制来处理其拷贝。需要说明的是,对于var1
(即QVector
的holder),对它整体的拷贝并没有使能QVariant
的shared状态(即它的is_shared
是0,shared->ref.loadRelaxed()
返回值是1000000000,跟实验1的结果一样)。怎会如此?我们来看看QVariant
的拷贝构造函数实现:QVariant::QVariant(const QVariant &p) : d(p.d) { if (d.is_shared) { d.data.shared->ref.ref(); } else if (p.d.type > Char) { handlerManager[d.type]->construct(&d, p.constData()); d.is_null = p.d.is_null; } }
可以看到,对于原来是
is_shared
的QVariant
,拷贝构造仅仅是对引用计数的加1;而对于原来不是is_shared
的QVariant
,对它的拷贝并不会使能shared状态,而是直接调用被存储数据的构造函数。因此,例子中的var
的ref
变成了2;var1
则是执行QVector
的拷贝构造函数。因为var1
本身的数据就支持隐式共享,所以最终的结果就是var
和var1
都是以常数时间完成了拷贝构造。 -
对比13和15行:对于数据的读取而言,
constData()
相比data()
的优势就体现出来了(即这里的castConstPtr()
与castPtr()
)。因为对于var
,执行data()
时真的进行了一次拷贝。所以才有了前面的建议:如果仅仅是为了读取数据,建议选用constData()。 -
对比14和16行:如前所述,因为
var1
依然不是shared状态,所以执行data()
时并不会执行拷贝。所以对它而言,constData()
和data()
没有区别。
3. 修改内部数据
这一节延续上一节的内容。我们将实现第二个需求,即直接对内部数据实现读改写。其实很简单,只需要先使用const_cast
强行将constData()
转换成void *
,然后再用reinterpret_cast
将void *
转换成目标类型的指针。用函数模板的实现代码如下:
template<typename T>
inline T *castMutablePtr(QVariant & var)
{
if (var.canConvert<T>()) {
return reinterpret_cast<T *>(const_cast<void *>(var.constData()));
}
return nullptr;
}
需要特别指出,这种方式是一把双刃剑,除非我们真的清楚我们正在做什么,否则尽量避免使用这种方式来直接修改QVariant
的内容。在接下来的实验中你明白为什么需要如此谨慎地对待它。另外,Qt的隐式共享机制已经足够强大了,可以应付大部分实际需求。
实践出真知,我们还是做个实验来验证一下。与上一节的设定类似,这里同样分别使用std::vector<uint8_t>
和QVector<int>
创建包含10亿个元素(每个元素的值设置为66)的实例,它们占用内存大小分别约等于953.74MB。我们先分别将数据放入QVariant
,并各自拷贝一份,然后使用的castMutablePtr()
获取数据指针并分别将容器的第一个元素的值修改为88。实验继续使用QElapsedTimer
统计关键操作的耗时。实验的关键代码如下:
QElapsedTimer profiler;
QVariant var;
QVariant var1;
std::vector<uint8_t> vec(1000000000, 66);
QVector<uint8_t> vec1(1000000000, 66);
var.setValue(vec);
var1.setValue(vec1);
QVariant varCopy(var);
QVariant var1Copy(var1);
profiler.start();
auto *ptrMut = castMutablePtr<std::vector<uint8_t>>(var);
auto consume1 = profiler.restart();
auto *ptr1Mut = castMutablePtr<QVector<uint8_t>>(var1);
auto consume2 = profiler.restart();
if (ptrMut != nullptr && ptr1Mut != nullptr) {
ptrMut->at(0) = 88;
auto consume3 = profiler.restart();
ptr1Mut->replace(0, 88);
auto consume4 = profiler.restart();
auto value = var.value<std::vector<uint8_t>>();
auto value1 = var1.value<QVector<uint8_t>>();
auto valueCopy = varCopy.value<std::vector<uint8_t>>();
auto value1Copy = var1Copy.value<QVector<uint8_t>>();
qInfo() << "ptrMut[0]=" << ptrMut->at(0);
qInfo() << "value[0]=" << value.at(0);
qInfo() << "valueCopy[0]=" << valueCopy.at(0);
qInfo() << "ptr1Mut[0]=" << ptr1Mut->at(0);
qInfo() << "value1[0]=" << value1.at(0);
qInfo() << "value1Copy[0]=" << value1Copy.at(0);
qInfo() << "[castMutablePtr] var consume" << consume1;
qInfo() << "[castMutablePtr] var1 consume" << consume2;
qInfo() << "[modify] ptrMut consume" << consume3;
qInfo() << "[modify] ptrMut1 consume" << consume4;
}
程序运行的输出结果如下:
ptrMut[0]= 88
value[0]= 88
valueCopy[0]= 88
ptr1Mut[0]= 88
value1[0]= 88
value1Copy[0]= 66
[castMutablePtr] var consume 0
[castMutablePtr] var1 consume 0
[modify] ptrMut consume 0
[modify] ptrMut1 consume 219
下面则解释一下实验的结果:
- 1-3行:可以看到,修改通过
castMutablePtr()
得到的指针指向的内容,导致var
和varCopy
的内容都受到了影响。所以说,一定要非常谨慎的使用这种方式。 - 4-6行:
var1
的情况和var
有所不同,修改通过castMutablePtr()
得到的指针指向的内容,仅仅影响了var1
的内容。这是因为隐式共享机制在这里做了一次copy on write,所以产生了这么大的耗时。 - 7-8行:上一个小节已经解释清楚了,这里无需赘述。
- 9-10行:因为对于不支持隐式共享的
std::vector
不会触发copy on write,节省了一次拷贝,所以第9行展示的耗时才会这么小。但它影响到了varCopy
的内容,所以说这是一把双刃剑。而第10行则如前面的解释,发生了copy on write,因此会有这么大的耗时。
4. 总结
本文介绍了使用QVariant::data()
和QVariant::constData()
直接访问QVariant
内容的方法,并通过实验证明了其有效性,同时也展现了其局限性。对于数据的读取操作,可以安心地使用QVariant::constData()
而无需担心副作用;而对于读改写操作,则需要非常谨慎地使用指针方式。