某日二师兄加入XXX科技公司的C++工程师开发岗位第16面:
面试官:什么是左值,什么是右值?
二师兄:简略来说,左值就是能够应用
&
符号取地址的值,而右值个别不能够应用&
符号取地址。
int a = 42; //a是左值,能够&aint* p = &a;int* p = &42; //42是右值,无奈取地址
二师兄:个别左值存在内存中,而右值存在寄存器中。
int a = 42, b = 1024;decltype(a+b); //类型为右值,a+b返回的值存在寄存器中decltype(a+=b); //类型为左值,a+=b返回的值存储在内存中
二师兄:严格意义上分,右值分为纯右值(pvalue
)和将亡值(xvalue
)。C++中,除了右值残余的就是左值。
42; //纯右值int a = 1024;std::move(a); //将亡值
面试官:C++98/03中曾经有了左值,为什么还要减少右值的概念?
二师兄:次要是为了效率。特地是
STL
中的容器,当须要把容器当作参数传入函数时:
void function(std::vector<int> vi2){ vi2.push_back(6); for(auto& i: vi2) { std:: cout < i << " " ;} std::cout << std::endl;}int main(int argc, char* argv[]){ std::vector<int> vi1{1,2,3,4,5}; function(vi1); return 0;}
二师兄:当咱们要把
vi1
传入函数时,在C++98/03时只能通过拷贝构造函数,把vi1
中所有的元素全副拷贝一份给vi2
,拷贝实现之后,当function
函数返回时,vi2
被析构,而后vi1
被析构。二师兄:在C++11及之后,咱们能够通过
std::move()
把vi1
强制转为右值,此时在初始化vi2
时执行的不是拷贝结构而是挪动结构:
void function(std::vector<int>&& vi2){ vi2.push_back(6); for(auto& i: vi2) { std:: cout < i << " " ;} std::cout << std::endl;}int main(int argc, char* argv[]){ std::vector<int> vi1{1,2,3,4,5}; function(std::move(vi1)); return 0;}
二师兄:这里只进行了一次结构。一次挪动(当元素特地多时,挪动的老本绝对于拷贝根本能够疏忽不记),一次析构。效率失去很大的晋升。
二师兄:当然,挪动过后的变量曾经不能再应用(身材被掏空),在
std::move(vi1)
之后应用vi1
是未定义行为。面试官:好的。那你晓得挪动结构是如何实现的吗?
二师兄:挪动结构是通过挪动构造函数实现的,当类有资源须要治理时,拷贝结构会把资源复制一份,而挪动结构偷走了原对象的资源。
struct Foo{ int* data_; //copy construct Foo(const Foo& oth) { data_ = new int(*oth.data_); } //move construct Foo(Foo&& oth) noexcept { data_ = oth.data_; //steal oth.data_ = nullptr; //set to null }}
面试官:好的。你感觉挪动构造函数的
noexcept
关键字能省略吗?为什么?二师兄:应该不能吧,具体不分明。
面试官:那你晓得std::move是如何实现的吗?
二师兄:如同是
static_cast
实现的吧。面试官:那你晓得什么叫万能援用吗?
二师兄:万能援用次要用在模板中,模板参数是
T
,形参是T&&
,此时能够传入任何类型的参数,所以称之为万能援用。
template<typename T>void function(T&& t) { ...}
面试官:那你晓得万能援用是如何实现的吗?
二师兄:不太分明。。
面试官:完满转发晓得吗?
二师兄:
std::forward
吗,理解过一些,不太熟悉。面试官:好的,回去等音讯吧。
让咱们来回顾以下二师兄明天的体现:
挪动构造函数的noexcept
关键字能省略吗?为什么?
这里尽量不要省略。如果省略,编译器会推断是否会抛出异样。如果挪动构造函数可能会抛出异样,则编译器不会将其标记为noexcept
。当编译器不标记为noexcept
时,为了保障程序的正确性,编译器可能会采纳拷贝结构的形式实现挪动结构,从而导致效率升高。
须要留神的是,如果标记了noexcept
但在挪动时抛出了异样,则程序会调用std::terminate()
函数来终止运行。
晓得std::move是如何实现的吗?
这里确实是通过static_cast实现的,讲左值强行转换成右值,用来匹配挪动语义而非拷贝。
template<typename T>typename std::remove_reference<T>::type&& move(T&& t) { return static_cast<typename std::remove_reference<T>::type&&>(t);}
万能援用是如何实现的?
万能援用次要应用了援用折叠技术,
template<typename T>void function(T&& t) { ...}
当T类型为左值时,&& &
被折叠为&
, 当T类型为右值时,&& &&
被折叠称为&&
。以下是折叠规定:
& & -> && && -> &&& & -> &&& && -> &&
完满转发晓得吗?
当咱们须要在function
中传递t参数时,如何保障它的左值或右值语义呢?这时候完满转发就退场了:
template<typename T>void function2(T&& t2) {}template<typename T>void function(T&& t) { function2(t);}
当传入的参数t的类型时右值时,因为援用折叠还是右值,此时的t
尽管时一个右值援用,但t
自身却是一个左值!这里十分的不好了解。如果咱们把t
间接传入到function2
,那么function2
中的t2
会被推导成左值,达不到咱们的指标。如果在调用function2
时传入std::move(t)
,当t
是右值时没有问题,但当t
是左值时,把t
挪动到t2
,t
在内部不在能用。这也不合乎咱们的预期。此时std::forward
闪亮退场!
template<typename T>void function2(T&& t2) {}template<typename T>void function(T&& t) { function2(std::forward<T&&>(t));}
std::forward
应用了编译时多态(SFINAE
)技术,使得当参数t
是左值是和右值是匹配不同的实现,实现返回不同类型援用的目标。以下是规范库的实现:
template <typename _Tp>constexpr _Tp && forward(typename std::remove_reference<_Tp>::type &&__t) noexcept{ return static_cast<_Tp &&>(__t);}template <typename _Tp>constexpr typename std::remove_reference<_Tp>::type && move(_Tp &&__t) noexcept{ return static_cast<typename std::remove_reference<_Tp>::type &&>(__t);}
好了,今日份面试到这里就完结了。二师兄的体现如何呢?预知后事如何,且听下回分解。
关注我,带你21天“精通”C++!(狗头)