为 const 引用和右值引用编写重载

问题描述 投票:0回答:3

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

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

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

例如考虑这个玩具示例:

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

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

例如,我正在考虑按照这样的移动版本来实现复制版本:

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 重载时会发生复制和移动操作,而不是之前所需的单个复制操作。

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

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

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

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

c++ c++11 move-semantics rvalue-reference perfect-forwarding
3个回答
5
投票

我的看法是,(真正)理解

std::move
std::forward
是如何工作的,以及它们的异同是解决你疑惑的关键,所以我建议你阅读我对有什么区别的回答在
std::move
std::forward
之间,我对两者给出了很好的解释。


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_assert
来强制
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
}

然后是 SFINAE,它更难,我想超出了本问答的范围。

我的建议

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

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


3
投票

一个简单的解决方案是:

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

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

模板的另一个解决方案是

std::forward
Enlico 的回答中进行了深入探讨。

如果您无法拥有模板并且移动的潜在成本太昂贵,那么您只需满足于具有两个重载的一些额外样板即可。


0
投票

您可以将两者都写成自动扣除参数类型的模板函数

template <class T>
void fooImpl(T && t )
{
    //when t is deduced as T const & the std::move is inocuous, 
    //when t is deduced as T && then the std::move actually moves
    globalVec.push_back(std::move(t)); 

}

void foo(const MyObject &obj) {
    fooImpl(obj); // Makes copy
}
void foo(MyObject &&obj) {
    fooImpl(obj); // Moves
}
© www.soinside.com 2019 - 2024. All rights reserved.