const 引用和右值引用的写入重载

Rav*_*ven 2 c++ rvalue-reference move-semantics perfect-forwarding c++11

最近,我发现自己经常遇到这样的情况:只有一个函数将某个对象作为参数。该函数必须复制该对象。

然而,该函数的参数也可能经常是临时的,因此我还想提供该函数的重载,该重载采用右值引用而不是常量引用。

两种重载的区别仅在于它们具有不同类型的引用作为参数类型。除此之外,它们在功能上是等效的。

例如,考虑这个玩具示例:

void foo(const MyObject &obj) {
    globalVec.push_back(obj); // Makes copy
}
void foo(MyObject &&obj) {
    globalVec.push_back(std::move(obj)); // Moves
}
Run Code Online (Sandbox Code Playgroud)

现在我想知道是否有一种方法可以通过例如根据另一个函数实现一个函数来避免这种代码重复。

例如,我正在考虑根据 move-one 实现复制版本,如下所示:

void foo(const MyObject &obj) {
    MyObj copy = obj;
    foo(std::move(copy));
}
void foo(MyObject &&obj) {
    globalVec.push_back(std::move(obj)); // Moves
}
Run Code Online (Sandbox Code Playgroud)

然而,这似乎仍然不理想,因为现在调用 const ref 重载时会发生复制和移动操作,而不是之前所需的单个复制操作。

此外,如果对象不提供移动构造函数,那么这将有效地将对象复制两次(据我所知),这违背了首先提供这些重载的整个目的(尽可能避免复制)。

我确信人们可以使用宏和预处理器将某些东西组合在一起,但我非常希望避免在其中涉及预处理器(出于可读性目的)。

因此,我的问题是:是否有可能实现我想要的目标(有效地仅实现一次功能,然后根据第一个重载实现第二个重载)?

如果可能的话,我想避免使用模板。

Enr*_*lis 5

我的观点是,(真正)理解std::move和是如何std::forward工作的,以及它们的相同点和不同点是解决您的疑问的关键,所以我建议您阅读我的回答 和 之间有什么区别std::movestd::forward,其中我给出了一个非常两者的很好的解释。


在

void foo(MyObject &&obj) {
    globalVec.push_back(obj); // Moves (no, it doesn't!)
}
Run Code Online (Sandbox Code Playgroud)

没有动静。obj是变量的名称,push_back将调用的重载不会从其参数中窃取资源。

你必须写

void foo(MyObject&& obj) {
    globalVec.push_back(std::move(obj)); // Moves 
}
Run Code Online (Sandbox Code Playgroud)

如果你想让移动成为可能,因为std::move(obj)说看,我知道这obj是一个局部变量,但我向你保证我以后不需要它,所以你可以将它视为临时的:如果需要的话窃取它的内脏。

至于您在中看到的代码重复

void foo(const MyObject &obj) {
    globalVec.push_back(obj); // Makes copy
}
void foo(MyObject&& /*rvalue reference -> std::move it */ obj) {
    globalVec.push_back(std::move(obj)); // Moves (corrected)
}
Run Code Online (Sandbox Code Playgroud)

让你避免它的是std::forward,你可以这样使用:

template<typename T>
void foo(T&& /* universal/forwarding reference -> std::forward it */ obj) {
    globalVec.push_back(std::forward<T>(obj)); // moves conditionally
}
Run Code Online (Sandbox Code Playgroud)

至于模板的错误消息,请注意有一些方法可以使事情变得更容易。例如,您可以static_assert在函数的开头使用 s 来强制其T为特定类型。这肯定会让错误更容易理解。例如:

#include <type_traits>
#include <vector>
std::vector<int> globalVec{1,2,3};

template<typename T>
void foo(T&& obj) {
    static_assert(std::is_same_v<int, std::decay_t<T>>,
                  "\n\n*****\nNot an int, aaarg\n*****\n\n");
    globalVec.push_back(std::forward<T>(obj));
}

int main() {
    int x;
    foo(x);
    foo(3);
    foo('c'); // errors at compile time with nice message
}
Run Code Online (Sandbox Code Playgroud)

然后是 SFINAE,它更难,我想超出了这个问题和答案的范围。

我的建议

不要害怕模板和 SFINAE!他们确实得到了回报:)

有一个漂亮的库,它大量且成功地利用了模板元编程和 SFINAE,但这确实是题外话:D

  • @Raven您可以定义一个在标头中定义的包装模板函数,该函数仅“static_assert”模板参数正确,然后委托给非内联模板。但此时,如果您的目标是减少样板文件,那么您就走错了方向。(或者是 SFINAE,但这也太可怕了) (2认同)