什么时候比rvalue参考模板更喜欢const lvalue引用

时间:2017-09-12 05:14:07

标签: c++ c++11 rvalue-reference perfect-forwarding

目前正在阅读cpr请求库的代码库: https://github.com/whoshuu/cpr/blob/master/include/cpr/api.h

注意到此库的接口经常使用完美转发。只是学习右值参考,所以这对我来说都是比较新的。

根据我的理解,rvalue引用,模板和转发的好处是被包围的函数调用将通过rvalue引用而不是值来获取其参数。这避免了不必要的复制。它还可以防止因参考推断而产生一堆重载。

然而,根据我的理解,const lvalue reference基本上做同样的事情。它可以防止需要重载并通过引用传递所有内容。需要注意的是,如果被包裹的函数采用非const引用,则无法编译。

但是如果调用堆栈中的所有内容都不需要非const引用,那么为什么不通过const lvalue引用传递所有内容呢?

我想我的主要问题是,你应该何时使用其中一个以获得最佳性能?尝试使用以下代码进行测试。得到以下相对一致的结果:

编译器:gcc 6.3 操作系统:Debian GNU / Linux 9

<<<<
Passing rvalue!
const l value: 2912214
rvalue forwarding: 2082953
Passing lvalue!
const l value: 1219173
rvalue forwarding: 1585913
>>>>

这些结果在运行之间保持相当一致。似乎对于一个rvalue arg,const l值签名稍微慢了,虽然我不确定为什么,除非我误解了这个并且const lvalue引用实际上做了rvalue的副本。

对于左值arg,我们看到计数器,右值转发速度较慢。为什么会这样?引用推论不应该总是产生对左值的引用吗?如果情况不是这样,它在性能方面不应该或多或少等于const左值参考?

#include <iostream>
#include <string>
#include <utility>
#include <time.h>

std::string func1(const std::string& arg) {
    std::string test(arg);
    return test;
}

template <typename T>
std::string func2(T&& arg) {
    std::string test(std::forward<T>(arg));
    return test;
}

void wrap1(const std::string& arg) {
    func1(arg);
}

template <typename T>
void wrap2(T&& arg) {
    func2(std::forward<T>(arg));
}

int main()
{
     auto n = 100000000;

     /// Passing rvalue
     std::cout << "Passing rvalue!" << std::endl;

     // Test const l value
     auto t = clock();
     for (int i = 0; i < n; ++i)
         wrap1("test");
     std::cout << "const l value: " << clock() - t << std::endl;

     // Test rvalue forwarding
     t = clock();
     for (int i = 0; i < n; ++i)
         wrap2("test");
     std::cout << "rvalue forwarding: " <<  clock() - t << std::endl;

     std::cout << "Passing lvalue!" << std::endl;

     /// Passing lvalue
     std::string arg = "test";

     // Test const l value
     t = clock();
     for (int i = 0; i < n; ++i)
         wrap1(arg);
     std::cout << "const l value: " << clock() - t << std::endl;

     // Test rvalue forwarding
     t = clock();
     for (int i = 0; i < n; ++i)
         wrap2(arg);
     std::cout << "rvalue forwarding: " << clock() - t << std::endl;

}

1 个答案:

答案 0 :(得分:3)

首先,here are与您的代码略有不同。正如评论中所提到的,编译器及其设置非常重要。特别是,您可能会注意到所有情况都具有相似的运行时间,但第一个情况除外,速度大约是其两倍。

Passing rvalue!
const l value: 1357465
rvalue forwarding: 669589
Passing lvalue!
const l value: 744105
rvalue forwarding: 713189

让我们看看每种情况下到底发生了什么。

1)当调用wrap1("test")时,由于该函数的签名需要const std::string &,所传递的char数组将在每次调用时隐式转换为临时std::string对象(即n次),其中涉及值的副本*。然后将对该临时文件的const引用传递到func1,其中构造另一个std::string,它再次涉及一个副本(因为它是一个const引用,它不能被移动,尽管事实上是暂时的)。即使函数按值返回,由于RVO,如果使用返回值,也可以保证复制将被省略。在这种情况下,不使用返回值,并且我不完全确定标准是否允许编译器优化temp的构造。我怀疑没有,因为一般来说这样的结构可能有可观察到的副作用(并且你的结果表明它没有被优化掉)。总而言之,在这种情况下,std::string的全面构造和销毁会执行两次。

2)调用wrap2("test")时,参数类型为const char[5],它作为右值引用转发到func2,其中std::string构造函数来自const char[]调用T来复制值。推导出的模板参数类型const char[5] &&const,很明显,尽管是左值参考,但仍无法移动它(由于std::string都不是const char[5])。与前一种情况相比,字符串的构造/销毁仅在每次调用时发生一次(wrap1(arg)文字始终在内存中并且不会产生任何开销。)

3)调用const string &时,您将左值作为func1通过链传递,并在wrap2(arg)中调用一个复制构造函数。

4)调用T时,这与之前的情况类似,因为const std::string &的推断类型为temp

5)我假设您的测试旨在展示在需要在调用链的底部创建参数副本时完美转发的优势(因此创建"test") 。在这种情况下,您需要在前两种情况下使用std::string("test")替换std::string &&参数,以便真正拥有std::forward<T>(arg)参数,并将您的完美转发修复为{{1} },如评论中所述。在这种情况下,the results是:

Passing rvalue!
const l value: 1314630
rvalue forwarding: 595084
Passing lvalue!
const l value: 712461
rvalue forwarding: 720338

这与我们以前的类似,但现在实际上调用了一个移动构造函数。

我希望这有助于解释结果。可能存在一些与内联函数调用和其他编译器优化相关的其他问题,这有助于解释案例2-4之间的较小差异。

关于你的问题使用哪种方法,我建议阅读Scott Meyer的#34;有效的现代C ++&#34;项目23-30。为书籍参考而不是直接答案道歉,但没有灵丹妙药,最佳选择始终取决于案例,因此最好只理解每个设计决策的权衡。

*由于短字符串优化,复制构造函数可能涉及或可能不涉及动态内存分配;感谢ytoledano在评论中提出这一点。此外,我在整个答案中都隐含地假设副本比移动要贵得多,但并非总是如此。