我最近听了Herb Sutter的一个演讲,他认为通过const &传递std::vector和std::string的原因已经基本消失了。他建议现在最好编写如下这样的函数:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

我知道return_val在函数返回时将是一个右值,因此可以使用move语义返回,这非常便宜。然而,inval仍然比引用(通常实现为指针)的大小大得多。这是因为std::string有各种组件,包括指向堆的指针和用于短字符串优化的成员char[]。所以在我看来,通过引用传递仍然是一个好主意。

谁能解释一下赫伯为什么会这么说?


当前回答

正如@ jduzgosz在评论中指出的那样,Herb在另一个(稍后?)谈话中给出了其他建议,大致可以从这里看到:https://youtu.be/xnqTKD8uD64?t=54m50s。

他的建议可以归结为,对于一个接受所谓汇聚参数的函数f,只使用值形参,假设您将从这些汇聚参数中移动construct。

与分别为左值和右值参数定制的f的最佳实现相比,这种通用方法只是同时为左值和右值参数增加了move构造函数的开销。要了解为什么会出现这种情况,假设f接受一个值形参,其中T是某个复制和移动构造类型:

void f(T x) {
  T y{std::move(x)};
}

使用左值参数调用f将导致调用一个复制构造函数来构造x,调用一个移动构造函数来构造y。另一方面,使用右值参数调用f将导致调用一个移动构造函数来构造x,并调用另一个移动构造函数来构造y。

一般来说,f对左值参数的最佳实现如下:

void f(const T& x) {
  T y{x};
}

在这种情况下,只调用一个复制构造函数来构造y。对于右值参数,f的最佳实现通常如下所示:

void f(T&& x) {
  T y{std::move(x)};
}

在这种情况下,只调用一个move构造函数来构造y。

因此,一个明智的妥协是,取一个value形参,并有一个额外的move构造函数调用,用于左值或右值参数,这也是Herb在演讲中给出的建议。

正如@ jdlugosz在评论中指出的那样,仅对将从sink参数构造某个对象的函数才有意义。当函数f复制其实参时,按值传递的方法比一般的按常量引用传递的方法开销更大。保留形参副本的函数f的值传递方法将具有如下形式:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

在这种情况下,左值实参有一个复制构造和一个move赋值,右值实参有一个move构造和move赋值。左值参数的最佳情况是:

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

这可以归结为仅进行赋值操作,这可能比值传递方法所需的复制构造函数加移动赋值要便宜得多。这样做的原因是赋值可能会重用y中现有的已分配内存,因此防止(取消)分配,而复制构造函数通常会分配内存。

对于右值实参,保留副本的f的最优实现形式为:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

这里只有一个move赋值。将右值传递给接受const引用的f版本只需要赋值,而不是move赋值。所以相对而言,在这种情况下,f的版本采用const引用作为通用实现更可取。

So in general, for the most optimal implementation, you will need to overload or do some kind of perfect forwarding as shown in the talk. The drawback is a combinatorial explosion in the number of overloads required, depending on the number of parameters for f in case you opt to overload on the value category of the argument. Perfect forwarding has the drawback that f becomes a template function, which prevents making it virtual, and results in significantly more complex code if you want to get it 100% right (see the talk for the gory details).

其他回答

几乎。

在c++ 17中,我们有basic_string_view<?>,这基本上把我们带到了std::string的一个狭窄的用例。

move语义的存在消除了std::string const&的一个用例——如果您计划存储参数,按值获取std::string是更优的,因为您可以移出参数。

如果有人用一个原始的C“string”调用你的函数,这意味着只有一个std::string缓冲区被分配,而不是std::string const&case中的两个。

然而,如果你不打算复制,通过std::string const&在c++ 14中仍然是有用的。

使用std::string_view,只要你没有将该字符串传递给一个期望c风格以“\0”结尾的字符缓冲区的API,你就可以更有效地获得类似std::string的功能,而无需承担任何分配风险。一个原始的C字符串甚至可以转换为std::string_view,而不需要任何分配或字符复制。

在这一点上,std::string const&的使用是当您不批量复制数据,并将其传递给一个c风格的API,该API期望一个以null结束的缓冲区,并且您需要std::string提供的高级字符串函数。在实践中,这是一组罕见的需求。

string不是普通旧数据(POD),它的原始大小不是最相关的东西。例如,如果传入的字符串超过SSO的长度,并且分配在堆上,我希望复制构造函数不复制SSO存储。

推荐这样做的原因是,inval是从参数表达式构造的,因此总是被适当地移动或复制——假设您需要参数的所有权,这不会造成性能损失。如果不这样做,const引用仍然是更好的方法。

我复制/粘贴了这个问题的答案,并更改了名称和拼写以适应这个问题。

下面是用来衡量问题的代码:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

对我来说,这输出:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

下表总结了我的结果(使用clang -std=c++11)。第一个数字是复制结构的数量,第二个数字是移动结构的数量:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

值传递解决方案只需要一次重载,但在传递左值和x值时需要额外的move构造。对于任何特定的情况,这可能是可接受的,也可能是不可接受的。这两种解决方案各有优缺点。

问题是“const”是一个非粒度限定符。“const string ref”通常的意思是“不要修改这个字符串”,而不是“不要修改引用计数”。在c++中,根本没有办法说哪些成员是“const”。它们要么都是,要么都不是。

为了解决这个语言问题,STL可以允许“C()”在你的例子中做一个移动语义复制,并在引用计数(可变)方面尽责地忽略“const”。只要它是指定好的,这就可以了。

因为STL没有,我有一个const_cast <>的字符串版本,去掉引用计数器(没有办法在类层次结构中追溯一些可变的东西),并且-你瞧-你可以自由地传递cmstring作为const引用,并在深层函数中复制它们,一整天,没有泄漏或问题。

由于c++在这里没有提供“派生类的const粒度”,编写一个好的规范并创建一个新的“const可移动字符串”(cmstring)对象是我见过的最好的解决方案。

IMO使用c++引用std::string是一个快速而简短的局部优化,而使用传递值可能是(或不是)一个更好的全局优化。

所以答案是:这取决于环境:

如果你把所有的代码从外部写到内部函数,你知道代码是做什么的,你可以使用引用const std::string &。 如果您编写库代码,或者在传递字符串的地方大量使用库代码,那么通过信任std::string复制构造函数行为,您可能会获得更多全局意义上的好处。