为 const 引用和 rvalue 引用编写重载

Posted

技术标签:

【中文标题】为 const 引用和 rvalue 引用编写重载【英文标题】:Write overloads for const reference and rvalue reference 【发布时间】:2021-09-17 18:41:57 【问题描述】:

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

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

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

例如考虑这个玩具示例:

void foo(const MyObject &obj) 
    globalVec.push_back(obj); // Makes copy

void foo(MyObject &&obj) 
    globalVec.push_back(std::move(obj)); // Moves

现在我想知道是否有办法避免这种代码重复,例如根据另一个功能实现一个功能。

例如,我正在考虑按照这样的 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

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

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

我确信可以使用宏和预处理器一起破解某些东西,但我非常希望避免让预处理器参与其中(出于可读性目的)。

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

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

【问题讨论】:

应该是globalVec.push_back(std::move(obj)); // Moves void foo(MyObject obj) globalVec.push_back(std::move(obj)); 可以避免过载。 啊,是的,我忘了在我的例子中写明确的std::move 只要你给一个右值一个名字,它就会变成一个 lvaue 所以obj 在这一行 globalVec.push_back(std::move(obj)); // Moves 是一个 lvaue 并且不会移动到向量中 你可能对我的桌子感兴趣here 【参考方案1】:

我的观点是,了解(真正)std::movestd::forward 的工作原理,以及它们的异同是解决你疑惑的关键,所以我建议你阅读我对@987654321 的回答@,我在这里对两者进行了很好的解释。


void foo(MyObject &&obj) 
    globalVec.push_back(obj); // Moves (no, it doesn't!)

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

你必须写

void foo(MyObject&& obj) 
    globalVec.push_back(std::move(obj)); // Moves 

如果你想让移动成为可能,因为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)

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

template<typename T>
void foo(T&& /* universal/forwarding reference -> std::forward it */ obj) 
    globalVec.push_back(std::forward<T>(obj)); // moves conditionally

关于模板的错误信息,请注意有一些方法可以让事情变得更简单。例如,您可以在函数的开头使用static_asserts 来强制T 是一个特定类型。这肯定会使错误更容易理解。例如:

#include <type_traits>
#include <vector>
std::vector<int> globalVec1,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

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

我的建议

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

There's a beautiful library that leverages template metaprogramming and SFINAE heavily and successfully,但这确实是题外话:D

【讨论】:

感谢您的回答。然后,这将需要将我的所有函数都转换为模板,这是我完全不喜欢的,因为必须在头文件中实现它们,而且还因为这些函数会产生可怕的错误消息......但是因为它回答了我仍然赞成的问题(尽管我仍然希望有其他选择) @Raven 如果只使用有限的模板实例化集,则不必在头文件中实现它们。 @463035818_is_not_a_number,这是原始评论。我会改的。 cmets 是骗子,我知道我为什么不喜欢他们 :P @Raven 您可以定义一个您在标头中定义的包装模板函数,只有static_asserts 模板参数是正确的,然后委托给非内联模板。但是在这一点上,如果您的目标是减少样板文件,那么您就走错了方向。 (或者是的 SFINAE,但这也很可怕)【参考方案2】:

一个简单的解决方案是:

void foo(MyObject obj) 
    globalVec.push_back(std::move(obj));

如果调用者传递一个左值,则有一个副本(进入参数)和一个移动(进入向量)。如果调用者传递一个右值,则有两个移动(一个到参数,另一个到向量)。由于额外的移动(由于缺乏间接性而略有补偿),与两个重载相比,这可能稍微不太理想,但在移动成本较低的情况下,这通常是一个不错的折衷方案。

模板的另一种解决方案是std::forward,在Enlico's answer 中进行了深入探讨。

如果您没有模板并且移动的潜在成本太高,那么您只需要满足一些额外的样板,即有两个重载即可。

【讨论】:

以上是关于为 const 引用和 rvalue 引用编写重载的主要内容,如果未能解决你的问题,请参考以下文章

返回 const 引用和 rvalue 引用之间的区别

为啥 const rvalue 限定 std::optional::value() 返回 const rvalue 引用?

C++,在函数中采用 const lvalue 和 rvalue 引用

模板中右值引用和 const 左值引用之间的重载

using 声明能否仅引用基于 const 限定的特定重载?

为啥不调用具有 const 引用返回值的重载方法?