常量引用 VS 移动语义

44
我想知道在C++11之后,仍需要在哪些情况下在参数中使用const引用。我并不完全了解移动语义,但我认为这是一个合法的问题。本问题仅针对一些情况,其中const引用替代了要进行复制但只需要“读取”值的情况(例如使用const成员函数)。
通常我会编写一个(成员)函数如下:
#include <vector>

template<class T>
class Vector {
    std::vector<T> _impl;
public:
    void add(const T& value) {
         _impl.push_back(value);
    }
};

但我认为可以安全地假设,如果我像这样编写它,并且class T当然实现了移动构造函数,那么编译器会使用移动语义进行优化:

#include <vector>

template<class T>
class Vector {
    std::vector<T> _impl;
public:
    void add(T value) {
         _impl.push_back(value);
    }
};

我理解的对吗?如果是这样,那么可以安全地假设它可以在任何情况下使用吗?如果不是,我想知道哪些情况下不能使用。 这将使生活变得更加轻松,因为我不需要为基本类型实现类专门化,此外,看起来更加清晰。


可能是在C++中传值还是传常量引用更好?的重复问题。 - stijn
1
vector::push_back 会复制对象。你需要使用 emplace_back - Cubic
4
@stijn,我在那个问题中找不到关于移动语义的类似情况。 - Tim
@Cubic 我想在将其推入vector的后面时进行复制。我想知道编译器是否能够通过移动语义(参见第二个示例)消除第二个副本。 - Tim
1个回答

46
您提出的解决方案:
void add(T value) {
     _impl.push_back(value);
}

需要进行一些调整,因为这种方式会始终执行一次 value 的复制,即使您将 rvalue 传递给 add()(如果您传递 lvalue,则会执行两次复制):由于 value 是一个 lvalue,当您将其作为参数传递给 push_back 时,编译器不会自动从中移动。
相反,您应该这样做:
void add(T value) {
     _impl.push_back(std::move(value));
//                   ^^^^^^^^^
}

这个代码已经比之前好了,但对于模板代码来说仍然不够好,因为你不知道 T 移动的成本是昂贵还是廉价。如果 T 是像这样的 POD:

struct X
{
    double x;
    int i;
    char arr[255];
};

那么移动它不会比复制更快(实际上,移动它就是复制它)。因为您的通用代码应该避免不必要的操作(这是因为对于某些类型来说,这些操作可能很昂贵),所以您不能通过值传递参数。

一个可能的解决方案(被C++标准库采用的方案)是提供两个add()的重载,一个使用左值引用,另一个使用右值引用:

void add(T const& val) { _impl.push_back(val); }
void add(T&& val) { _impl.push_back(std::move(val)); }

另一种可能性是提供一个(可能受SFINAE约束的)完美转发模板版本的add(),它将接受所谓的万能引用(由Scott Meyers创造的非标准术语):
template<typename U>
void add(U&& val) { _impl.push_back(std::forward<U>(val)); }

这两种解决方案都是最优的,因为当提供lvalues时只执行一次复制操作,当提供rvalues时只执行一次移动操作。


谢谢,正是我所需要的!关于“但对于模板代码仍不够好”和“移动它与复制它是一样的”,这难道不是你第一个引用的陈述的完全相反吗?我的意思是,这难道不正是我想要实现的,使用相同的方法来获得没有开销的基本类型。 - Tim
@Tim: 我在那里没有看到矛盾之处。“对于模板代码仍然不够好”是指函数通过值获取其参数,通常情况下不够好,因为参数可能具有可能昂贵的类型(需要复制或移动),因此您希望避免任何额外的(复制或)移动。 X旨在提供此类类型的示例(实际上,是否真的“昂贵”取决于客户的要求,但让我们不要沉迷于这种考虑中)。 - Andy Prowl
1
@Tim:我不确定我理解了。push_back()有两个重载版本,一个总是复制,一个总是移动,它们都需要引用。如果你按值传递,函数参数将被复制或移动(取决于其值类别)到函数参数中 - 这是你在处理模板代码时应该避免的额外操作,因为你事先不知道这个操作的开销有多大。 - Andy Prowl
对于最后一个代码示例,当然要读取 emplace_back 而不是 push_back - norisknofun
1
在第一个可能的解决方案的第二行中,即void add(T&& val) { _impl.push_back(std::move(val)); }中,显式的std::move是否真的必要?val已经是T&&类型,即右值引用。因此,正确的重载版本的push_back应该移动其参数,甚至可以在没有显式std::move的情况下调用吗? - user2690527
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接