万能引用

左值和右值

std::move&std::forward

之前也写过std::move&std::forward相关的文章,也是写完后好久没看过了,最近学习模板的相关知识,发现以前的文章理解比较浅薄且存在问题,在此重新进行分析。

std::forward

表示完美转发,能将万能引用的形参进行完美转发。
直接上样例:

#include <iostream>
#include <type_traits>

template<class T>
constexpr T&& Forward(std::remove_reference_t<T>& arg) noexcept {
std::cout << "&" << std::endl;
return static_cast<T&&>(arg);
}

template<class T>
constexpr T&& Forward(std::remove_reference_t<T>&& arg) noexcept {
std::cout << "&&" << std::endl;
return static_cast<T&&>(arg);
}

void test(int& a) {
std::cout << "test &" << std::endl;
}

void test(int&& a) {
std::cout << "test &&" << std::endl;
}

template<class T>
void print(T&& t) {
// test(t);
// test(Forward<T>(t));
}

int main() {
int a = 1;
print(a);
print(2);
return 0;
}

样例中给出了std::forward的可能实现Forward,其中一个重载的参数类型是左值引用,另一个重载的参数类型是右值引用。这意味着传入的左值会调用左值引用的重载,传入的右值会调用右值引用的重载。以左值引用的重载进行分析。

template<class T>
constexpr T&& Forward(std::remove_reference_t<T>& arg) noexcept {
std::cout << "&" << std::endl;
return static_cast<T&&>(arg);
}

std::remove_reference_t<T>表示去除类型T中的引用类型。如果T是一个左值引用类型U&,通过引用折叠的规则,函数的返回类型T&&会变成左值引用U&,返回值变成static_cast<U&>(arg),整个函数模板实例化后,可以看成下面这种形式,此时的arg通过Forward处理后,输出的仍然是一个左值arg

template<class U>
constexpr U& Forward(U& arg) noexcept {
std::cout << "&" << std::endl;
return static_cast<U&>(arg);
}

如果T是一个右值引用类型U&&,整个函数模板实例化后,可以看成下面这种形式,此时的arg通过Forward处理后,输出的仍然是一个右值arg

template<class U>
constexpr U&& Forward(U& arg) noexcept {
std::cout << "&&" << std::endl;
return static_cast<U&&>(arg);
}

我们再看样例中的函数模板print

template<class T>
void print(T&& t) {
// test(t);
// test(Forward<T>(t));
}

其中的T&& t是一个万能引用。
万能引用:如果一个变量或者参数被声明为T&&,其中T是可以被推导的类型,那这个变量或者参数就是一个万能引用。如果用来初始化万能引用的表达式是一个左值,那么万能引用就变成了左值引用,如果用来初始化万能引用的表达式是一个右值,那么万能引用就变成了右值引用。
当传入的实参是左值,T会被推导为U&,引用折叠后的t是左值引用类型。当传入的实参是右值,T会被推导为U&&,引用折叠后的t是右值引用类型。当传入的实参是引用类型(左值引用或右值引用),函数模板会忽略其引用类型,例如传入的实参如果是int& a,相当于传入int a

现在考虑完整的样例,函数模板print中实现为:

template<class T>
void print(T&& t) {
test(t);
}

输出结果为:

test &
test &

这里调用两次print,一次传入的是左值,一次传入的是右值,当传入左值,我们希望内部调用左值引用的test,当传入右值,我们希望调用的是右值引用的test,但实际上调用的都是左值引用的test。我们来看下传入右值的情况,当右值传入到print,此时的形参t虽然是右值引用类型,但此时的变量t是一个左值,所以调用的也只能是左值引用的test。我们想传入testt保持和传入print的实参具有同样的值性,即左值仍然是左值,右值仍然是右值。这个时候就需要用到完美转发,现在函数模板print的实现为:

template<class T>
void print(T&& t) {
test(Forward<T>(t));
}

输出结果为:

&
test &
&
test &&

利用Forward我们得到了想要的结果,我们再对传入右值进行分析。此时t仍是一个左值,调用的是左值引用的Forward,此时的T也会被推导为U&&,即我们会调用Forward<U&&>(U&),此时会返回一个右值,即传入test的是一个右值,调用的也就是右值引用的test

std::move

我们直接看一个实例:

#include <iostream>
#include <type_traits>

template<class T>
constexpr std::remove_reference_t<T>&& Move(T&& arg) noexcept {
return static_cast<std::remove_reference_t<T>&&>(arg);
}

void test(int& a) {
std::cout << "test &" << std::endl;
}

void test(int&& a) {
std::cout << "test &&" << std::endl;
}

int main() {

int a = 2;
test(a);
test(Move(a));

return 0;
}

实例中的Movestd::move的可能实现。T&& arg是一个万能引用,std::remove_reference_t<T>能去除T的引用类型,返回值是将arg强转成了右值引用类型,返回类型也是一个右值引用,通过Move我们能得到一个右值。
上述样例的输出为:

test &
test &&

变量a原本是一个左值,Move(a)返回了一个右值,符合我们的预期。为啥需要std::move,因为我们需要将一个左值转换成右值,为啥需要右值,因为我们需要支持移动语义。