直接向std::string内部缓冲区写入数据

44

我正在寻找一种方法,在DLL界限上将一些数据塞入字符串。由于我们使用不同的编译器,所以所有的dll接口都是简单的char*。

有没有一种正确的方法可以将指针传递到DLL函数中,使它能够直接填充字符串缓冲区?

string stringToFillIn(100, '\0');
FunctionInDLL( stringToFillIn.c_str(), stringToFillIn.size() );   // definitely WRONG!
FunctionInDLL( const_cast<char*>(stringToFillIn.data()), stringToFillIn.size() );    // WRONG?
FunctionInDLL( &stringToFillIn[0], stringToFillIn.size() );       // WRONG?
stringToFillIn.resize( strlen( stringToFillIn.c_str() ) );

看起来最有希望的方法是 &stringToFillIn[0],但考虑到你可能认为 string::data() == &string[0],这样做是否正确呢?它似乎不一致。

或者我们应该接受额外的分配并回避这个问题:

vector<char> vectorToFillIn(100);
FunctionInDLL( &vectorToFillIn[0], vectorToFillIn.size() );
string dllGaveUs( &vectorToFillIn[0] );
9个回答

26

我不确定标准是否保证了std::string中的数据存储为char*。我能想到的最可移植的方式是使用std::vector,它保证将其数据存储在连续的内存块中:

std::vector<char> buffer(100);
FunctionInDLL(&buffer[0], buffer.size());
std::string stringToFillIn(&buffer[0]);

当然,这将需要将数据复制两次,这有点低效。


7
就效率而言,如果您开始使用std::vector作为缓冲区,您将遇到一种不同类型的性能问题,即向量的每个元素都会逐个初始化。如果您预留一个32K的缓冲区(这并不多),您将花费相当多的CPU时间来初始化此缓冲区。如果您只需要连续的内存块,您最好仅使用一个新的char[]数组,结合std::unique_ptr或其他RAII模式即可,但除非您绝对需要初始化每个元素,否则不要使用std::vector。 - John Leidegren
2
在https://dev59.com/oWgu5IYBdhLWcg3wso6j中使用vector<uninitialized_char>技巧。 - Marc Eaddy
2
“我不确定标准是否保证std::string中的数据是存储为char*。” 实际上是有保证的。 std::string 使用char。http://en.cppreference.com/w/cpp/string/basic_string - cambunctious

22
更新(2021年):C++11已经解决了这个问题,这里提出的担忧不再相关。
经过更多阅读和深入挖掘,我发现string::c_str和string::data可以合法地返回指向与字符串本身存储方式无关的缓冲区的指针。例如,字符串可能是分段存储的。对这些缓冲区进行写入将对字符串的内容产生未定义的影响。
此外,string::operator[]不能用于获取字符序列的指针-它只能用于单个字符。这是因为指针/数组等价性在字符串中不成立。
这种做法非常危险的原因是它在一些实现上可能有效,但在将来的某个时间突然失效而没有明显的原因。
因此,像其他人所说的那样,唯一安全的方法是避免任何直接写入字符串缓冲区的尝试,使用矢量,传递指向第一个元素的指针,然后从dll函数返回时将字符串从矢量赋值。

38
C++0x会改变字符串的存储方式,使其使用连续的内存空间。 - Patrick
2
帕特里克说的话。此外,Herb Sutter 在2008年讨论C++0x工作组时,并不知道任何非连续的实现方式:http://herbsutter.wordpress.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/ (向下滚动查看评论)。 - Steve Jessop
有趣的是他说&str[0]应该给出连续的数据,而Stroustrup在《C++程序设计语言特别版》20.3.3 p585中似乎表示相反。也许我误解了它。期待C++0x来清理这个混乱!我们认为我们依赖于代码各个部分中不被技术上保证的行为,因此我们将添加一些测试,如果我们对字符串的假设不再成立,这些测试将会失败。唉。 - markh44
20.3.3中的那一点(在我的第三版中)涉及到略微不同的事情 - 数组/指针等价性也不适用于向量。我认为Sutter并没有与Stroustrup相矛盾,而且无论如何,在编写参考工作时,您都希望尽可能避免使用“目前,我所看到的所有实现都执行X”这种形式的语句,而是使用“标准保证Y,请勿假设X”。当您讨论标准的未来版本和/或编写仅需要在Sutter知道的实现上运行的实际代码时,情况会有所不同... - Steve Jessop
1
我同意你的“在实际生活中测试”方法。如果在使用时你断言&s[0] + (s.size()-1) == &s[s.size()-1],那么你应该是没问题的,尽管我认为即使字符串存储在多个单独的分配中,也有可能通过极端的不幸而成立。对于向量给出的保证是&v[n] == &v[0] + n,其中n从0到length()-1。 - Steve Jessop

12

在C++98中,不应该修改由string::c_str()string::data()返回的缓冲区。此外,正如其他回答中所解释的那样,不能使用string::operator[]来获取指向字符序列的指针 - 它只能用于单个字符。

从C++11开始,字符串使用连续的内存,因此您可以使用&string[0]来访问内部缓冲区。


8
只要C++11提供了连续内存保证,在生产实践中,这种“hacky”方法非常流行:
std::string stringToFillIn(100, 0);
FunctionInDLL(stringToFillIn.data(), stringToFillIn.size());

1
感谢Orion Edwards的编辑。请注意,此内容仅适用于C++17标准。有关详细信息,请参见http://en.cppreference.com/w/cpp/string/basic_string/data。 - Brian Cannard
3
仅补充一下Brian的评论,C++17添加了一个非const的data()重载函数,特别允许这种行为。 - Marcus10110
函数调用后,“stringToFillIn”仍将具有长度为100。 - Thomas Weller

3

考虑到Patrick的评论,我会说,直接写入std::string是可以的,而且很方便/高效。我会使用&s.front()来获取一个char *,就像在这个mex示例中一样:

#include "mex.h"
#include <string>
void mexFunction(
    int nlhs,
    mxArray *plhs[],
    int nrhs,
    const mxArray *prhs[]
)
{
    std::string ret;
    int len = (int)mxGetN(prhs[0]);
    ret.reserve(len+1);
    mxGetString(prhs[0],&ret.front(),len+1);
    mexPrintf(ret.c_str());
}

2
不应在空字符串上调用 string.front(),请使用 &string[0] - Andrei Bozantan
对我来说,这应该是 ret.resize 而不是 ret.reserve - Malachi

3

我不会构造一个 std::string 并跨 dll 边界发送指向内部缓冲区的指针。相反,我会使用简单的 char 缓冲区(静态或动态分配)。在调用 dll 返回后,我会让 std::string 接管结果。让被调用者写入内部类缓冲区感觉直观上是错误的。


2
您可以使用在unique_ptr中分配的char缓冲区,而不是使用向量:

// allocate buffer
auto buf = std::make_unique<char[]>(len);
// read data
FunctionInDLL(buf.get(), len);
// initialize string
std::string res { buf.get() };

您不能使用以下方式直接向字符串缓冲区中写入内容:&str[0]str.data()

#include <iostream>
#include <string>
#include <sstream>

int main()
{
    std::string str;
    std::stringstream ss;
    ss << "test string";
    ss.read(&str[0], 4);       // doesn't work
    ss.read(str.data(), 4);    // doesn't work
    std::cout << str << '\n';
}

实时示例


ss.write() 不会起作用,我想你的意思是 ss.read(),否则这里就没有意义了,对吧? - Alexis Wilke
@AlexisWilke,好的,谢谢您的纠正。 - isnullxbh

0

std::string 的标准部分是 API 和一些行为,而不是实现的内存布局。

因此,如果您使用不同的编译器,则不能假设它们相同,因此您需要传输实际数据。正如其他人所说,传输字符并推入新的 std::string


0

你们已经解决了连续性问题(即不能保证连续),所以我只想提一下分配/释放点的问题。过去我曾遇到过在动态链接库中分配内存(即由动态链接库返回字符串)导致在销毁时(在动态链接库之外)出现错误的情况。为了解决这个问题,你必须确保你的分配器和内存池在动态链接库边界上是一致的。这将节省你一些调试时间;)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接