可以覆盖std :: string的空终止符吗?

41
在C++11中,我们知道std::string保证是连续的并且以null结尾(或者更加准确地说,以charT()结尾,在char的情况下是null字符0)。
我需要使用这个C API填写字符串。它通过指针写入整个字符串和null结尾符。在C++03中,我总是被迫使用一个vector<char>,因为我不能假定string是连续的或者以null结尾。但是在C++11中(假设一个正确遵守的basic_string类,这在一些标准库中仍然是棘手的),我可以做到。
那么我真的可以吗?当我这样做时:
std::string str(length);
字符串将分配length+1个字节,其中最后一个由空终止符填充。这很好。但是当我将其传递给C API时,它将写入length+1个字符。它将覆盖空终止符。
诚然,它将使用空字符覆盖空终止符。很有可能这会起作用(实际上,我无法想象它怎么可能不会起作用)。
但我不关心"能不能用",我想知道,根据规范,在null-terminator上覆盖一个null字符是否可以?

5
好的,@NicolBolas的问题不是“它会引起问题吗”,而是“规范允许吗”。 - nneonneo
2
换句话说,如果在您的系统上可行,谁会关心规范允许什么?幸运的是,并非每个人都持有这种态度。 - user743382
3
如果我没有漏掉什么,使用长度为“length+1”的带有额外空字符的std::string应该可以避免这个问题,对吗? - user743382
7
@texasbruce,这完全不相关。重点是标准中没有任何保证空终止符在可写内存位置上。例如,它完全可能(尽管不太可能)位于只读内存中。然后,任何尝试写入它的操作都会使程序崩溃。任何能干的 C 程序员都会告诉您,如果您尝试编写忽略这些影响的可移植程序,那么您非常疯狂。这根本不是“完全正常”的事情。 - Konrad Rudolph
4
同意。但这是完全不同的讨论。即使这样,忽略规范也没有好处:你仍然可以有意决定破坏规范 - 但首先你应该知道它。 - Konrad Rudolph
显示剩余13条评论
4个回答

25
很遗憾,如果我理解正确的话,这是UB(未定义行为),但无论如何都不被允许:
§21.4.5 [string.access] p2
返回:如果pos < size(),则*(begin() + pos),否则返回一个类型为T且值为charT()的对象的引用;被引用的值不得修改。
(编者错误,它应该说charT而不是T。)
.data()和.c_str()基本上指回了operator[] (§21.4.7.1 [string.accessors]p1):
返回:一个指针p,使得对于每个i在[0,size()]中,p + i == &operator[](i)。

8
将 '\0' 写入已经是 '\0' 的内容中,是否算作修改它? - Michael Anderson
6
@MichaelAnderson,是的,确实如此。它会写入内存。 - Jonathan Wakely
2
@KonradRudolph 好吧,datac_str返回const指针,所以它们不适合修改。从21.4.5 p2中并不能真正推断出*(&str[0] + str.size())是允许的,因为[]仅对于pos < size()等于*(begin()+pos)。我认为实现可以完全允许在length数组中保存字符串数据,并带有额外的static const charT成员用于空值(当然这意味着它必须维护一个额外的缓冲区通过datac_str返回,但为什么不呢?)。 - Christian Rau
2
@KonradRudolph:必须这样做。缓冲区是连续的。因此,&str[str.size() - 1] == &str[str.size()] - 1 必须为真(假设 length() 至少为1)。如果不是这样,那么缓冲区就不会是连续的。 - Nicol Bolas
3
@NicolBolas 不行。 size()operator[] 的无效参数,因此不能保证其返回值指向缓冲区。 例如(牵强附会),operator[] 可以包含以下逻辑:static CharT terminator{}; if (index == size()) return terminator; else return _data[i]; - Konrad Rudolph
显示剩余10条评论

12

LWG 2475 修改了 operator[](size()) 的规范,从而使得这个操作合法(粗体字为新增内容):

否则,返回一个类型为 charT 的对象引用,其值为 charT()。将该对象修改为任何不是 charT() 的值都将导致未定义行为。


2
@M.M. 这个信息隐藏在 data() 函数的规范中。 - T.C.
1
我认同如果使用 data() 作为指针的来源,那么一切都好,但是我看不到 &s[0] 给出了同样的保证。具体而言,我看不到任何防止实现在实际调用 data()c_str()(并将其写入或使用哑元进行 &s[s.size()] 的情况)之前不写入空终止符的东西。 - M.M
我不知道为什么在过去时中提到了LWG 2475,状态报告显示它尚未解决。 - Ben Voigt
在提案的注释中写道:“周二下午:这也应该适用于非const data()。Billy更新措辞。”看起来这还没有发生,如果通过指针算术而不是调用字符串的operator[]()进行覆盖终止符仍然是非法的。 - Ben Voigt
@BenVoigt:应该注意到,2475的当前状态(更新于今天)是“C++17”。因此,委员会似乎认为它已经得到解决并添加到了C++17中。 - Nicol Bolas
显示剩余3条评论

11
根据规范,覆盖终止的NUL应该是未定义的行为。因此,正确的做法是在字符串中分配length+1个字符,将字符串缓冲区传递给C API,然后将其resize()length
// "+ 1" to make room for the terminating NUL for the C API
std::string str(length + 1);

// Call the C API passing &str[0] to safely write to the string buffer
...

// Resize back to length
str.resize(length);

(顺便说一句,我在 MSVC10 上尝试了“覆盖 NUL”的方法,并且它可以正常工作。)


2
我也会选择这个解决方案。但令人不满的是,这需要完全不必要地分配额外的字符。为什么规范没有将空终止符设为可写呢? - Konrad Rudolph
因为那意味着要多打字?“你不能覆盖空值,除非用另一个空值替换它。”也许是他们的手指累了,或者是一天结束了,酒吧开门了。 - Martin James
@KonradRudolph:我同意应该更改标准,使得可以用另一个NUL覆盖它。我看不出为什么不可能或者为什么会触发未定义的行为,并且我也不喜欢不必要地多分配一个字符的情况。 - Mr.C64

5
我认为n3092已经不再是当前版本,但这就是我所拥有的。21.4.5节允许访问单个元素。它要求pos <= size()。如果pos < size(),则可以获得实际元素,否则(即如果pos == size()),则会得到一个不可修改的引用。
我认为在编程语言方面,一种可以修改值的访问方式被认为是一种修改,即使新值与旧值相同。
g++是否有一个严谨的库可以链接到?

libstdc++有一个调试模式,但它的诊断能力是有限的。它可以验证迭代器操作,但无法注意到通过指针写入单个字节的情况。 - Jonathan Wakely

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