Qt 库与 STL 库混用的隐患

由于我在混用 Qt 与 STL 库时遇到了一些奇怪的错误.
经过检查实际上是因为两个库实现逻辑上有所冲突, 故写此文.
我会分点记录遇到的这些”冲突”, 后续有新发现持续更新.

1. Qt / C++ / C 三种风格字符串转换 ( QString::toStdString().data() )

案情

当一个方法需要传入一个 const char* 参数, 而此时数据保存在 QString 对象内时.
我最初的做法是使用 QString::toStdString().data() 先将 QString 转换为 const char*, 再将转换后的 C 字符串传入方法.
这么做导致参数的数据丢失了.

现场大概是这个样子:

1
2
3
4
5
6
7
8
9
10

void func(const char* cStr) {
// do something
}

QString qtStr = "confidential information";
const char* cStr = qStr.toStdString().data();

func(cStr); // cStr 的数据丢失

调查

这里检查了一下 QString::toStdString()std::string::data() 两个方法的实现机制.

QString::toStdString() 的实现机制

QString::toStdString() 内部实现中, 最终构造并返回了一个 std::string 对象. 其中携带的数据也在构造时进行了拷贝. 因此在方法执行后, 这里的 Qt 字符串与 C++ 字符串二者之间不再有任何”纠葛”.

以下内容截取自 Qt 5.14.2 官方文档:

1
2
3
4
5
std::string QString::toStdString() const
Returns a std::string object with the data contained in this QString.
The Unicode data is converted into 8-bit characters using the toUtf8() function.
This method is mostly useful to pass a QString to a function that accepts a std::string object.
See also toLatin1(), toUtf8(), toLocal8Bit(), and QByteArray::toStdString().

std::string::c_str() 的实现机制

以下代码段截取自 std::string 的 STL 源码 (GCC) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// base_string.h

template<typename _CharT, typename _Traits, typename _Alloc>
class basic_string
{
......

public:

// String operations:
/**
* @brief Return const pointer to null-terminated contents.
*
* This is a handle to internal data. Do not modify or dire things may
* happen.
*/
const _CharT*
c_str() const _GLIBCXX_NOEXCEPT
{ return _M_data(); }

......

}

注意注释部分, 此处返回的指针是指向 std::string 内部数据的指针. 实际上 _M_data() 返回的是 traits, 但最后我们还是可以得到指针. 但无论如何, 这里没有对数据进行拷贝.

PS: 顺嘴说一句, std::string::data()std::string::c_str() 两种方法至少在 C++17 之前, 都是完全一致的. 二者都是调用 std::string::_M_data().

破案

结合两个方法的实现机制可知, 当执行 QString::toStdString().data() 时, 首先构造了一个 std::string 的临时对象, 再构造一个指向该对象内部数据的 const char* 指针并返回. 这个指针最终赋值给我所声明的那个 C 字符串.

问题的关键就在于: 这个临时的 std::string 对象本身没有被任何地方引用, 因此它将在这行语句结束之后被析构. 而析构它的同时自然也释放了其内部的数据. 此时我的这个 C 字符串实际上失去了关联, 使用其传参自然是无法获得所需数据的.

总结

根据上文可知, QString::toStdString().data() 这个转换的方式本身有问题, 关键在于转换时的 std::string 临时对象没有被引用.

我遇到的问题涉及到了方法传参, 若非要用这种方式转换, 则应该直接将该转换步骤放在参数列表中, 保证临时对象在方法执行结束之前都处于被引用的状态. 或是直接用一个 std::string 变量保存数据, 在最终需要传值的时候调用 std::string::data().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

void func(const char* cStr) {
// do something
}

QString qtStr = "confidential information";

// error : 临时 std::string 对象被析构

const char* cStr = qtStr.toStdString().data();
func(cStr);

// OK

std::string stdStr = qtStr.toStdString();
const char* cStr = stdStr.data();
func(cStr);

// OK

func(qtStr.toStdString().data());

疑问

我尝试过将 QString 转为 QByteArray, 再将 QByteArray 转为 const char*. 这么做最后得到的 C 字符串却是拥有数据的, 但是 QByteArray::constData() 本身理应是不做深拷贝的, 具体原因暂时未知. 可能与 Qt 内部某些机制有关.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

void func(const char* cStr) {
// do something
}

QString qtStr = "confidential information";

// OK

const char* cStr = qtStr.toUtf8().constData(); // 此处没有进行深拷贝, 为何成功取得数据?
func(cStr);

// 为证明没有进行深拷贝, 将拷贝过程与 QByteArray 临时变量放在方法内, 此时果然cStr没有正常取得数据

void testFunc(const char* cStr) {
QString qtStr = "confidential information";
cStr = qtStr.toUtf8().constData();
}

const char* cStr = nullptr;
testFunc(cStr); // 没有正常取得数据
func(cStr); // error