今天我通过了这个问题:Legality of COW std::string implementation in C++11
该问题的最多投票答案(35票赞成)说:
这是不允许的,因为根据标准21.4.1 p6,失效 只允许
使用迭代器/引用- 作为参考的任何标准库函数的参数 将非const basic_string作为参数。
- 调用非const成员函数,除了operator [],at,front, back,begin,rbegin,end和rend。
对于COW字符串,调用非const运算符[]需要创建一个 复制(和无效的引用),这是不允许的 以上段落。因此,拥有COW字符串已不再合法 C ++ 11
我想知道这种理由是否有效,因为C ++ 03似乎对字符串迭代器失效有类似的要求:
引用a的元素的引用,指针和迭代器 basic_string序列可能会被以下用途无效 basic_string对象:
- 作为非成员函数swap()(21.3.7.8),运算符>>()(21.3.7.9)和getline()(21.3.7.9)的参数。
- 作为basic_string :: swap()的参数。
- 调用data()和c_str()成员函数。
- 调用非const成员函数,除了operator [](),at(),begin(),rbegin(),end()和rend()。
- 除了返回迭代器的insert()和erase()形式之外的任何上述用法之后,第一次调用非const成员 函数operator [](),at(),begin(),rbegin(),end()或rend()。
这些与C ++ 11的不完全相同,但operator[]()
的部分至少相同,原始答案作为主要理由。所以我想,为了证明C ++ 11中COW std :: string实现的非法性,需要引用其他一些标准要求。需要帮助。
这个SO问题已经停止了一年多,所以我决定将其作为一个单独的问题提出来。如果这不合适,请告诉我,我会找到其他方法来澄清我的疑问。
答案 0 :(得分:27)
关键点是C ++ 03标准的最后一点。该
措辞可能会更清楚,但意图是第一个
之后拨打[]
,at
等(但仅限第一次通话)
建立新迭代器的东西(因而无效)
旧的)可以使迭代器无效,但只有第一个。该
事实上,C ++ 03中的措辞很快就被插入了
回应法国国家机构关于CD2的评论
C ++ 98。最初的问题很简单:考虑:
std::string a( "some text" );
std::string b( a );
char& rc = a[2];
此时,rc
的修改必须影响a
,但是
不是b
。但是,如果使用COW,则在调用a[2]
时,
a
和b
分享代表;为了写通过
返回的引用不会影响b
,a[2]
必须是
被认为是“写”,并被允许无效
参考。这是CD2所说的:对非const的任何调用
[]
,at
或其中一个begin
或end
函数可以
使迭代器和引用无效。法国国家机构
评论指出这使得a[i] == a[j]
无效,
因为其中一个[]
返回的引用将是
由另一个无效。你引用C ++ 03的最后一点是
为了避免这种情况 - 仅限第一次调用[]
等
人。可以使迭代器无效。
我认为没有人对结果感到满意。该
措辞很快就完成了,虽然意图很明确
那些了解历史和原始问题的人,
我不认为标准是完全清楚的。此外,
一些专家开始质疑COW的价值,
鉴于字符串类本身的相对不可能性
可靠地检测所有写入。 (如果a[i] == a[j]
是完整的
表达,没有写。但字符串类本身必须
假设a[i]
的返回值可能导致写入。)
而在多线程环境中,管理成本
写入时复制所需的引用计数被认为是相对的
你通常不需要的东西的高成本。结果是
大多数实现(很久以前就支持线程)
无论如何,C ++ 11)已经远离COW;我所知道的,
仍然使用COW的唯一主要实现是g ++(但在那里
是多线程实现中的已知错误)和
(也许)Sun CC(我最后一次看到它的时候是
因为管理柜台的成本太慢了。
我认为委员会只是采取了他们认为的那样
通过禁止COW,最简单的清理方式。
关于COW实施原因的更多说明
必须在第一次调用[]
时使迭代器无效。考虑
COW的天真实施。 (我将它称为String,和
忽略涉及特征和分配器的所有问题,这些问题
在这里并不重要。我也会忽略异常和
线程安全,只是为了让事情尽可能简单。)
class String
{
struct StringRep
{
int useCount;
size_t size;
char* data;
StringRep( char const* text, size_t size )
: useCount( 1 )
, size( size )
, data( ::operator new( size + 1 ) )
{
std::memcpy( data, text, size ):
data[size] = '\0';
}
~StringRep()
{
::operator delete( data );
}
};
StringRep* myRep;
public:
String( char const* initial_text )
: myRep( new StringRep( initial_text, strlen( initial_text ) ) )
{
}
String( String const& other )
: myRep( other.myRep )
{
++ myRep->useCount;
}
~String()
{
-- myRep->useCount;
if ( myRep->useCount == 0 ) {
delete myRep;
}
}
char& operator[]( size_t index )
{
return myRep->data[index];
}
};
现在想象如果我写下会发生什么:
String a( "some text" );
String b( a );
a[4] = '-';
此后b
的价值是多少? (通过代码运行
如果你不确定,请亲自动手。)
显然,这不起作用。解决方案是添加一个标志,
bool uncopyable;
到StringRep
,已初始化为
false
,并修改以下功能:
String::String( String const& other )
{
if ( other.myRep->uncopyable ) {
myRep = new StringRep( other.myRep->data, other.myRep->size );
} else {
myRep = other.myRep;
++ myRep->useCount;
}
}
char& String::operator[]( size_t index )
{
if ( myRep->useCount > 1 ) {
-- myRep->useCount;
myRep = new StringRep( myRep->data, myRep->size );
}
myRep->uncopyable = true;
return myRep->data[index];
}
这当然意味着[]
将使迭代器无效
引用,但仅在第一次在对象上调用时。
下一次,useCount
将是一个(图像将是
不可复制)。所以a[i] == a[j]
有效;无论哪个
编译器实际上首先评估(a[i]
或a[j]
),第二个
一个人会发现useCount
为1,并且不需要复制。
而且由于uncopyable
标志,
String a( "some text" );
char& c = a[4];
String b( a );
c = '-';
也可以使用,而不是修改b
。
当然,上述内容极大地简化了。得到它 在多线程环境中工作非常困难, 除非你只是为任何一个函数获取整个函数的互斥量 可以修改任何东西的函数(在这种情况下, 结果类非常慢)。 G ++尝试过,和 失败 - 有特定的用例,它会中断。 (让它来处理我忽略的其他问题不是 特别困难,但确实代表了许多方面 代码。)