【问题标题】:std::string& vs boost::string_refstd::string& 与 boost::string_ref
【发布时间】:2017-01-08 21:44:03
【问题描述】:

如果我使用 boost::string_ref 而不是 std::string& 有什么关系吗?我的意思是,在处理字符串时使用boost::string_ref 是否真的比标准版本更有效?我真的不明白这里提供的解释:http://www.boost.org/doc/libs/1_61_0/libs/utility/doc/html/string_ref.html。真正让我困惑的是,std::string 也是一个句柄类,它只指向分配的内存,并且从 c++11 开始,使用移动语义,上面文章中提到的复制操作不会发生。那么,哪一个更高效呢?

【问题讨论】:

    标签: c++ c++11 boost


    【解决方案1】:

    string_ref(或最近的 Boost 和 C++17 中的string_view)的用例用于子字符串引用

    情况

    • 源字符串恰好是std::string
    • 并且引用了源字符串的全长

    是一个(非典型)特殊情况,它确实类似于std::string const&

    注意另外string_ref(如sref.substring(...))的操作会自动返回更多string_ref对象,而不是分配一个新的std::string

    【讨论】:

      【解决方案2】:

      我从来没有使用过它,在我看来它的目的是提供一个类似于std::string 的接口,但不必分配一个字符串来进行操作。以给定extract_part() 的示例为例:给定一个硬编码的C 数组"ABCDEFG",但由于初始函数采用std::string,因此发生分配(std::string 将有自己的"ABCDEFG" 版本)。使用string_ref,不会发生分配,它使用对初始"ABCDEFG" 的引用。约束是字符串是只读的。

      【讨论】:

      • 但是“const boost::string_ref”和“const std::string&”类型的函数处理参数有什么区别?
      • 我认为最好阅读@sehe 的回答,该回答描述了在 string_ref 上调用 substring 返回另一个 string_ref 而不是新的 std::string。
      【解决方案3】:

      此答案使用新名称string_viewstring_ref 表示相同。

      真正让我困惑的是std::string也是一个句柄类,它只指向分配的内存

      string 分配、拥有和管理自己的内存。 string_view 是一些已分配内存的句柄。内存由其他机制管理,与string_view 无关。

      如果您已经有一些文本数据,例如在char 数组中,那么构造string 所涉及的额外内存分配可能是多余的。 string_view 可能更有效,因为它允许您直接对 char 数组中的原始数据进行操作。但是,它不允许修改数据; string_view 不允许非const 访问,因为它不拥有它所引用的数据。

      从 c++11 开始,使用移动语义,上面文章中提到的复制操作不会发生。

      您只能从准备好被丢弃的对象移动。复制仍然是有目的的,并且在很多情况下是必要的。

      文章中的示例构造了两个新的strings(不是副本),还构造了现有strings 的两个副本。在 C++98 中,RVO 已经可以在没有移动语义的情况下删除副本,因此它们没什么大不了的。通过使用string_view,它避免了构造两个新的strings。移动语义在这里无关紧要。

      在对extract_part("ABCDEFG") 的调用中,构造了一个string_view,它引用由字符串文字表示的char 数组。在此处构造string 将涉及内存分配和char 数组的副本。

      在对bar.substr(2,3) 的调用中,构造了一个string_view,它引用了第一个string_view 已经引用的部分数据。在此处使用string 将涉及另一次内存分配和部分数据的复制。

      那么,哪个效率更高?

      这有点像问锤子是否比螺丝刀更有效。它们有不同的用途,所以这取决于你想要完成什么。

      在使用 string_view 时需要小心,它所引用的内存在其整个生命周期内都保持有效。

      【讨论】:

        【解决方案4】:

        如果你坚持std::string 没关系,但boost::string_ref 也支持const char*。也就是说,你打算只用std::string调用你的字符串处理函数foo吗?

        void foo(const std::string&);
        
        foo("won't work"); // no support for `const char*`
        

        由于boost::string_ref 可以从const char* 构造,因此它更灵活,因为它同时适用于const char*std::string

        建议N3442 可能会有所帮助。

        【讨论】:

        • 它会工作,但会导致临时的std::string对象创建(因为std::string::string(const char*)构造函数不是explicit
        • @milleniumbug 请通过编辑将您的评论添加到帖子中并删除您的评论,我也会删除此评论。
        • @milleniumbug 然后我会删除它。我想改进 StackOverflow 和更好的答案/删除无效答案有助于这样做。
        【解决方案5】:

        简而言之std::string_view 优于 const std::string& 的主要优点是您可以同时传递 const char*std::string 对象而无需进行复制。正如其他人所说,它还允许您在不复制的情况下传递子字符串,尽管(根据我的经验)这不太重要。

        考虑以下(愚蠢的)函数(是的,我知道你可以直接调用 s.at(2)):

        char getThird(std::string s)
        {
            if (s.size() < 3) throw std::runtime_error("String too short");
            return s[2];
        }
        

        此函数有效,但字符串是按值传递的。这意味着即使我们没有查看所有字符串,也会复制字符串的整个长度,并且它也(通常)会导致动态内存分配。在一个紧密的循环中执行此操作可能非常昂贵。一种解决方案是通过 const 引用传递字符串:

        char getThird(const std::string& s);
        

        如果您有一个std::string 变量并将其作为参数传递给getThird,这会更好。但是现在有一个问题:如果你有一个以空结尾的const char* 字符串怎么办?当您调用此函数时,将构造一个临时的std::string,因此您仍然可以获得副本和动态内存分配。

        这是另一个尝试:

        char getThird(const char* s)
        {
            if (std::strlen(s) < 3) throw std::runtime_error("String too short");
            return s[2];
        }
        

        这显然现在适用于const char* 变量。它也适用于std::string 变量,但调用它有点尴尬:getThird(myStr.c_str())。更重要的是,std::string 支持嵌入的空字符,getThird 会将字符串误解为以第一个字符结尾。在最坏的情况下,这可能会导致安全漏洞 - 想象一下,如果该函数被称为 checkStringForBadHacks

        另一个问题是,用旧的空终止字符串而不是std::string 对象及其方便的方法编写函数很烦人。例如,您是否注意到,即使只有前几个字符很重要,此函数也会查看字符串的整个长度?它隐藏在std::strlen 中,它遍历所有字符以寻找空终止符。我们可以将其替换为手动检查前三个字符是否为空,但您会发现这比其他版本要方便得多。

        进入std::string_view(或boost::string_view,以前称为boost::string_ref):

        char getThird(std::string_view s)
        {
            if (s.size() < 3) throw std::runtime_error("String too short");
            return s[2];
        }
        

        这为您提供了您期望从适当的字符串类(如.size())获得的好方法,并且它适用于上述两种情况,以及另一种情况:

        • 它适用于std::string 对象,可以隐式转换为std::string_view 对象。
        • 它适用于const char*以null结尾的字符串,也可以隐式转换为std::string_view对象。
          • 这确实有一个潜在的缺点,即构造std::string_view 需要遍历整个字符串以找到长度,即使使用它的函数从不需要它(就像这里的情况一样)。但是,如果调用者使用 const char* 作为多个函数(或循环中的一个函数)的参数,这些函数采用 std::string_view 对象,它总是可以事先手动构造该对象。这甚至可以提高性能,因为如果该函数确实需要长度,那么它会被预先计算一次并重复使用。
        • 正如其他答案所提到的,当您只想传递子字符串时,它也会避免复制。例如,这在解析中非常有用。但是std::string_view 即使没有这个功能也是合理的。

        值得注意的是,在某些情况下,原始函数签名(按值取 std::string)实际上可能比 std::string_view 更好。无论如何,这就是您要复制字符串的地方,例如存储在其他变量中或从函数返回。想象一下这个函数:

        std::string changeThird(std::string s, char c)
        {
            if (s.size() < 3) throw std::runtime_error("String too short");
            s[2] = c;
            return s;
        }
        
        // vs.
        
        std::string changeThird(std::string_view s, char c)
        {
            if (s.size() < 3) throw std::runtime_error("String too short");
            std::string result = s;
            result[2] = c;
            return result;
        }
        

        请注意,这两个都只涉及一个副本:在第一种情况下,当参数s 由传入的任何内容构造时(包括如果它是另一个std::string),这是隐式完成的。在第二种情况下,我们在创建result 时明确地执行此操作。但是 return 语句不做复制,因为使用了移动语义(就像我们做了std::move(result)),或者更有可能使用return value optimisation

        第一个版本可能更好的原因是它实际上可以执行零拷贝,如果调用者移动参数:

        std::string something = getMyString();
        std::string other = changeThird(std::move(something), "x");
        

        在这种情况下,第一个 changeThird 根本不涉及任何副本,而第二个则涉及。

        【讨论】:

        • 恐怕 'raise' 在 C++ 中不是关键字 - 它看起来像 Python :-)。你能解释一下或修复它(只需使用throw)吗?
        • 哎呀,你是对的 - 在 C++ 和 Python 之间切换时,我总是忘记这一点!感谢您指出:-)
        猜你喜欢
        • 1970-01-01
        • 2011-09-17
        • 1970-01-01
        • 2010-11-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-08-22
        相关资源
        最近更新 更多