【问题标题】:Possibility of COW std::string implementation in C++11C++11 中 COW std::string 实现的可能性
【发布时间】:2014-04-04 13:40:25
【问题描述】:

今天我通过了这个 SO 问题:Legality of COW std::string implementation in C++11

对该问题投票最多的答案(35 个赞成票)是:

这是不允许的,因为根据标准 21.4.1 p6,失效 迭代器/引用只允许用于

——作为任何标准库函数的参数,引用 以非常量 basic_string 作为参数。

——调用非常量成员函数,除了 operator[], at, front, 返回、开始、重新开始、结束和结束。

对于 COW 字符串,调用非常量 operator[] 需要创建一个 复制(并使引用无效),这是不允许的 上段。因此,在其中包含 COW 字符串不再合法 C++11。

我想知道这个理由是否有效,因为似乎 C++03 对字符串迭代器无效有类似的要求:

引用 a 元素的引用、指针和迭代器 basic_string 序列可能会因以下用途而失效 基本字符串对象:

  • 作为非成员函数 swap() (21.3.7.8)、operator>>() (21.3.7.9) 和 getline() (21.3.7.9) 的参数。
  • 作为 basic_string::swap() 的参数。
  • 调用 data() 和 c_str() 成员函数。
  • 调用非常量成员函数,除了 operator[]()、at()、begin()、rbegin()、end() 和 rend()。
  • 在上述任何使用之后,除了返回迭代器的 insert() 和 erase() 的形式,第一次调用非常量成员 函数 operator[]()、at()、begin()、rbegin()、end() 或 rend()。

这些与 C++11 的不完全相同,但至少与 operator[]() 的部分相同,原始答案将其作为主要理由。所以我想,为了证明 C++11 中 COW std::string 实现的非法性,需要引用其他一些标准要求。这里需要帮助。


这个 SO 问题已经有一年多没有活跃了,所以我决定将其作为一个单独的问题提出。如果这不合适,请告诉我,我会找到其他方法来消除我的疑问。

【问题讨论】:

  • C++03 中引用段落下方的注释指出规则打算允许 COW 实现.. 我认为最后一个要点也有一个例外 第一次打电话给非会员operator[],但不知怎么的,我发现这个表述很不清楚。
  • @dyp:是的,在 03 和 11 之间这些无效要求确实存在差异,这些差异意味着打算在 03 中使奶牛成为可能,在 11 中使奶牛成为不可能,但到目前为止我还不能找出逻辑链,至少不是链接的 SO 问题的原始答案中所说的。我会尝试阅读您和 pmr 提供的 open-std 文档,看看我是否能得到一些想法。
  • 认为(但目前无法证明)C++03 允许第一次调用operator[] 来复制字符串并使任何引用到已调用 operator[] 的字符串。

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


【解决方案1】:

关键点是C++03标准的最后一点。这 措辞可能更清晰,但意图是第一个 之后拨打[]at等(但仅限第一次通话) 建立新迭代器的东西(因此无效 旧的)可以使迭代器无效,但只有第一个。这 实际上,C++03 中的措辞是一种快速破解,插入 法国国家机构在 CD2 上对 cme​​ts 的回应 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 所说的:对非常量的任何调用 []atbeginend 函数之一可以 使迭代器和引用无效。法国国家机构 cmets 指出这导致 a[i] == a[j] 无效, 因为[] 之一返回的引用将是 被对方无效。您引用 C++03 的最后一点是 添加以规避这一点-仅对[]等的第一次调用 人。可能会使迭代器无效。

我认为没有人对结果完全满意。这 措辞很快就完成了,虽然意图很明确 那些了解历史和原始问题的人, 我认为标准中并没有完全清楚。此外, 一些专家开始质疑 COW 的价值, 鉴于字符串类本身相对不可能 可靠地检测所有写入。 (如果a[i] == a[j] 是完整的 表达式,没有写。但是字符串类本身必须 假设a[i] 的返回值可能会导致写入。) 在多线程环境中,管理 写时复制所需的引用计数被认为是相对的 您通常不需要的东西的高成本。结果是 大多数实现(很久以前就支持线程 C++11) 无论如何都在远离 COW;据我所知, 唯一仍在使用 COW 的主要实现是 g++(但有 是他们的多线程实现中的一个已知错误)和 (也许)Sun CC(我最后一次看它时,是 由于管理柜台的成本,非常慢)。 我认为委员会只是把他们认为的 最简单的清理方法,就是禁止 COW。

编辑:

关于为何使用 COW 实施的更多说明 必须在第一次调用 [] 时使迭代器无效。考虑 COW 的幼稚实现。 (我只称它为字符串,并且 忽略所有涉及特征和分配器的问题,这 在这里并不重要。我也会忽略异常和 线程安全,只是为了让事情尽可能简单。)

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++ 尝试过,并且 失败——在特定的用例中它会中断。 (让它处理我忽略的其他问题不是 特别难,但确实代表了很多行 代码。)

【讨论】:

  • 非常感谢您的回答。还是有点糊涂。通过说the first call to [], at, etc. (but only the first call) **after something which established new iterators** (and thus invalidated old ones) could invalidate iterators, but only the first.,在调用a[2] 之前,您的示例中的something which established new iterators 是什么? a的构造?作为b 构造的参数传递时可能调用data()
  • 感谢您对此的历史观点。
  • @JamesKanze 谢谢,这正是我正在寻找的那种例子,而且在 Delphi 中的表现也不尽如人意。 var S1: string; procedure X(var C: Char); var S2: string; begin S2 := S1; C := 'x'; ShowMessage(S2); end; S1 := 'abc'; X(S1[2]); 显示 axc。 (我相信 Delphi 语法的可读性足以让人理解。)
  • 将可修改字符串的后备存储公开为指针(可修改或不可修改)要求在此类公开之前存在字符串的不可共享后备存储。将与低于 500 万亿个字符的字符串相关的一些创建成本推迟到需要公开后备存储时,这不是合法的吗?由于许多字符串永远不需要暴露其后备存储,因此在某些情况下这似乎是一种潜在的性能“胜利”。创建大于 500 万亿个字符的字符串会比 499 万亿个要慢,但是...
  • ...公开任何字符串的时间是 O(1),创建一个长度为 N 的字符串并将其公开 M 次以获得较小值的成本将是 O(N)+O (M)。
猜你喜欢
  • 2012-08-25
  • 1970-01-01
  • 2021-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-19
相关资源
最近更新 更多