首页 文章

如何从函数中返回一个对象,考虑C 11 rvalues并移动语义?

提问于
浏览
362

我正在尝试理解rvalue引用并移动C 11的语义 .

这些示例之间有什么区别,哪些不会执行矢量复制?

第一个例子

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

第二个例子

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

第三个例子

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

5 回答

  • 15

    它们都不会复制,但第二个会引用一个被破坏的矢量 . 命名的右值引用几乎从不存在于常规代码中 . 你只需要在C 03中写一份副本就可以了 .

    std::vector<int> return_vector()
    {
        std::vector<int> tmp {1,2,3,4,5};
        return tmp;
    }
    
    std::vector<int> rval_ref = return_vector();
    

    除了现在,向量移动 . 在绝大多数情况下,类的用户不会使用rvalue引用 .

  • 40

    简单的答案是你应该为常规引用代码编写rvalue引用的代码,并且你应该在99%的时间内在精神上对它们进行相同的处理 . 这包括有关返回引用的所有旧规则(即永远不会返回对局部变量的引用) .

    除非您正在编写需要利用std :: forward并且能够编写采用左值或右值引用的泛型函数的模板容器类,否则这或多或少都是正确的 .

    移动构造函数和移动赋值的一个重要优点是,如果定义它们,编译器可以在RVO(返回值优化)和NRVO(命名返回值优化)无法调用的情况下使用它们 . 这对于通过方法有效地返回昂贵的对象(如容器和字符串)非常重要 .

    现在,rvalue引用让事情变得有趣,你也可以将它们用作普通函数的参数 . 这允许您编写具有const引用(const foo和other)和rvalue引用(foo && other)重载的容器 . 即使参数过于笨拙而无法通过构造函数调用,它仍然可以完成:

    std::vector vec;
    for(int x=0; x<10; ++x)
    {
        // automatically uses rvalue reference constructor if available
        // because MyCheapType is an unamed temporary variable
        vec.push_back(MyCheapType(0.f));
    }
    
    
    std::vector vec;
    for(int x=0; x<10; ++x)
    {
        MyExpensiveType temp(1.0, 3.0);
        temp.initSomeOtherFields(malloc(5000));
    
        // old way, passed via const reference, expensive copy
        vec.push_back(temp);
    
        // new way, passed via rvalue reference, cheap move
        // just don't use temp again,  not difficult in a loop like this though . . .
        vec.push_back(std::move(temp));
    }
    

    STL容器已更新为几乎任何东西都有移动重载(散列键和值,矢量插入等),并且您将在这里看到它们 .

    您也可以将它们用于普通函数,如果只提供右值引用参数,则可以强制调用者创建对象并让函数执行移动 . 这是一个非常好用的例子,但在我的渲染库中,我为所有加载的资源分配了一个字符串,这样就可以更容易地看到每个对象在调试器中代表什么 . 界面是这样的:

    TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
    {
        std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
        tex->friendlyName = std::move(friendlyName);
        return tex;
    }
    

    它是一种“漏洞抽象”的形式,但允许我利用我必须在大多数时间创建字符串的事实,并避免再次复制它 . 这不是完全高性能的代码,但是当人们掌握这一功能时,这是一个很好的例子 . 这段代码实际上要求变量是调用的临时变量,或调用std :: move:

    // move from temporary
    TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));
    

    要么

    // explicit move (not going to use the variable 'str' after the create call)
    string str("Checkerboard");
    TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));
    

    要么

    // explicitly make a copy and pass the temporary of the copy down
    // since we need to use str again for some reason
    string str("Checkerboard");
    TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));
    

    但这不会编译!

    string str("Checkerboard");
    TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
    
  • 2

    本身不是答案,而是指南 . 在大多数情况下,声明本地 T&& 变量没有多大意义(就像你使用 std::vector<int>&& rval_ref 一样) . 您仍然需要 std::move() 才能在 foo(T&&) 类型方法中使用它们 . 还有一个问题已经提到,当你试图从函数返回这样的 rval_ref 时,你将得到标准的被引用到被破坏的临时惨败 .

    大多数时候我会采用以下模式:

    // Declarations
    A a(B&&, C&&);
    B b();
    C c();
    
    auto ret = a(b(), c());
    

    您没有对返回的临时对象持有任何引用,因此您可以避免(没有经验的)程序员的错误,他们希望使用移动的对象 .

    auto bRet = b();
    auto cRet = c();
    auto aRet = a(std::move(b), std::move(c));
    
    // Either these just fail (assert/exception), or you won't get 
    // your expected results due to their clean state.
    bRet.foo();
    cRet.bar();
    

    显然有(虽然很少见)一个函数真正返回 T&& 的情况,它是对可以移动到对象中的非临时对象的引用 .

    关于RVO:这些机制通常可以工作,编译器可以很好地避免复制,但是在返回路径不明显的情况下(异常, if 条件确定您将返回的命名对象,可能还有其他)rrefs是您的救星(即使可能更贵) .

  • 3

    第一个例子

    std::vector<int> return_vector(void)
    {
        std::vector<int> tmp {1,2,3,4,5};
        return tmp;
    }
    
    std::vector<int> &&rval_ref = return_vector();
    

    第一个示例返回一个由 rval_ref 捕获的临时 . 那个临时的生命将超出 rval_ref 定义,你可以使用它,好像你已经按 Value 捕获它 . 这非常类似于以下内容:

    const std::vector<int>& rval_ref = return_vector();
    

    除了在我的重写中,你显然不能以非const方式使用 rval_ref .

    第二个例子

    std::vector<int>&& return_vector(void)
    {
        std::vector<int> tmp {1,2,3,4,5};
        return std::move(tmp);
    }
    
    std::vector<int> &&rval_ref = return_vector();
    

    在第二个示例中,您创建了一个运行时错误 . rval_ref 现在持有对函数内部被破坏的 tmp 的引用 . 运气好的话,这段代码会立即崩溃 .

    第三个例子

    std::vector<int> return_vector(void)
    {
        std::vector<int> tmp {1,2,3,4,5};
        return std::move(tmp);
    }
    
    std::vector<int> &&rval_ref = return_vector();
    

    你的第三个例子大致相当于你的第一个例子 . tmp 上的 std::move 是不必要的,实际上可能是性能下降,因为它会抑制返回值优化 .

    编码您正在做的事情的最佳方法是:

    最好实践

    std::vector<int> return_vector(void)
    {
        std::vector<int> tmp {1,2,3,4,5};
        return tmp;
    }
    
    std::vector<int> rval_ref = return_vector();
    

    即就像你在C 03中那样. tmp 在return语句中被隐含地视为rvalue . 它将通过返回值优化(无复制,无移动)返回,或者如果编译器决定它不能执行RVO,则它will use vector's move constructor to do the return . 仅当未执行RVO时,并且如果返回的类型没有移动构造函数,则复制构造函数将用于返回 .

  • 497

    这些都不会做任何额外的复制 . 即使没有使用RVO,新标准也表示移动结构在做回报时比我认为的更好 .

    我相信你的第二个例子会导致未定义的行为,因为你正在返回对局部变量的引用 .

相关问题