为什么不允许通过检索的指向字符串数据的指针来修改字符串?

27
在C++11中,std::string 的字符必须被存储为连续的,正如§21.4.1/5所指出的:
  

一个 basic_string 对象内的类 char 对象应被作连续存储。也就是说,对于任意一个 basic_string 对象 s,在所有满足 0 <= n < s.size() 的 n 值上都有 &*(s.begin() + n) == &*s.begin() + n.

然而,这里是§21.4.7.1列出的两个函数来检索指向底层存储的指针(重点在于我):
  

const charT *c_str() const noexcept;
  const charT *data() const noexcept;
  1 返回:一个指针 p,使得对于每个 i 在 [0,size()] 中,p + i == &operator[](i).
  2 复杂度:常数时间。
  3 要求:程序不得更改存储在字符数组中的任何值。

我能想到的关于第三点的一个可能性是,该指针可以因对象的以下用途而无效(§21.4.1/6):
  • 作为任何标准库函数的参数,这些函数将非 const basic_string 的引用作为参数。
  • 调用非 const 成员函数,除了 operator[]、at、front、back、begin、rbegin、end 和 rend。
即使如此,迭代器也可能会无效,但在它们失效之前我们仍然可以修改它们。我们仍然可以使用指针读取缓冲区直到指针失效。
为什么不能直接写入该缓冲区?是因为这会将类置于不一致的状态中,例如,end() 将无法随新末尾更新吗?如果是这样,为什么可以直接写入像 std::vector 这样的东西的缓冲区呢?
这样做的用例包括能够将 std::string 的缓冲区传递给 C 接口以检索字符串,而不是传递一个 vector<char> 并使用该向量的迭代器初始化字符串:
std::string text;
text.resize(GetTextLength());
GetText(text.data());

2
挑刺:一个好的 C API 也应该带有长度,所以它应该是GetText(text.data(), text.size()); :P - Nawaz
@Nawaz,True,我应该将其调整大小为 length + 1,但我决定不仅仅为了这个而去编辑它。 - chris
1
@Nawaz 一个非常好的观点。 - WhozCraig
@chris 哈哈,我也是直到现在才知道,觉得值得一试,而且在预览中看起来不错 =P - WhozCraig
也许他们不想说“程序不应将字符数组的任何元素设置为,从而使对象的内部状态无效”。 - s.bandara
显示剩余4条评论
1个回答

37

为什么我们不能直接写入这个缓冲区?

很明显,因为它是const。强制转换一个const值并修改其中的数据是... 粗鲁的。

那么为什么它是const呢?这要追溯到当时写时复制被认为是一个好主意的时代,所以std::basic_string必须允许实现支持它。获取一个不可变的字符串指针(例如用于传递给C-API)而又不产生拷贝的开销将非常有用。因此,c_str需要返回一个const指针。

至于为什么它仍然是const呢?嗯... 这与标准中的一个奇怪的东西有关:空终止符。

以下是合法的代码:

std::string stupid;
const char *pointless = stupid.c_str();

pointless 必须是一个以 NUL 结尾的字符串。具体来说,它必须是指向 NUL 字符的指针。那么 NUL 字符从哪里来呢?std::string 实现有几种方法可以使其工作:

  1. 使用小字符串优化,这是一种常见技术。在这种方案中,每个 std::string 实现都有一个内部缓冲区可用于单个 NUL 字符。
  2. 返回指向包含 NUL 字符的静态内存的指针。因此,如果是空字符串,则每个 std::string 实现都将返回相同的指针。

不应强制每个人都实现 SSO。因此标准委员会需要一种方式来保留第二种方法。这部分的解决方案之一就是通过 c_str() 给你提供一个 const 字符串。由于这块内存很可能是真正的 const,而不是假的“请勿修改此内存”类型的 const,所以给你一个可变指针是不好的想法。

当然,你仍然可以通过 &str[0] 来获取这样的指针,但是标准非常明确,修改 NUL 终止符是一种不好的做法

当然,如果你仅在半开范围 [0,str.size()) 内进行操作,则通过修改 &str[0] 指针和其中字符的数组是完全有效的。只是不能通过 datac_str 返回的指针进行操作。即使标准实际上要求 str.c_str() == &str[0] 成立。

这就是标准术语。


1
我正想问是否可以让API覆盖空终止符,但是后来我看了你的链接:p无论如何,我传递&str[0]也很高兴:) - chris
2
@chris:「允许」和「礼貌」是有区别的。const 对象是你和其他代码之间的契约。如果你取消了 const,那么你就违反了这个契约。虽然在某些情况下语言可能允许这样做,但是对于给你提供该对象并告诉你不要触摸它的任何代码来说,这是不礼貌的。如果有人告诉你不要坐在他们的沙发上,而你却坐了下来,他们可能不会把你赶出去。但他们也不会对此表示好意。 - Nicol Bolas
当然可以通过datac_str返回的指针来完成,因为你重新表述的保证。该保证确保返回的指针指向可修改的内存(对于除0终止符之外的所有内容,如果您没有使用c_str则无需设置)。 - Deduplicator
1
有很多情况下,“写时复制”仍然是一个好主意。嵌入式系统实现可能会从识别字符串是由存储在ROM中的文本构建而来并使字符串对象标识该文本而不是为其分配堆对象而受益;复制其文本存储在ROM中的字符串或其中一部分不需要为文本的副本分配新的堆存储。将写时复制应用于RAM中的内容可能过于复杂,无法证明其价值,但如果实现可以使用特定于实现的... - supercat
1
通过确定字符串是否存储在ROM中,对这些情况应用写时复制可能比应用到RAM字符串更容易,但仍然提供了很多好处[不仅仅是速度——写时复制将使得小型嵌入式系统能够使用存储在ROM中的字符串,这些字符串比设备的整个RAM还要大!]。 - supercat
显示剩余3条评论

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