在C ++ 11中实现COW std :: string的可能性

时间:2014-03-03 13:21:23

标签: c++ string c++11 language-lawyer standard-library

今天我通过了这个问题: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问题已经停止了一年多,所以我决定将其作为一个单独的问题提出来。如果这不合适,请告诉我,我会找到其他方法来澄清我的疑问。

1 个答案:

答案 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]时, ab分享代表;为了写通过 返回的引用不会影响ba[2]必须是 被认为是“写”,并被允许无效 参考。这是CD2所说的:对非const的任何调用 []at或其中一个beginend函数可以 使迭代器和引用无效。法国国家机构 评论指出这使得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 ++尝试过,和 失败 - 有特定的用例,它会中断。 (让它来处理我忽略的其他问题不是 特别困难,但确实代表了许多方面 代码。)