前面说过,c++标准其实是分成两块推进的。一块是语言标准,另外一块是应用库标准。线程同步还麻烦呢,别说两个组的大佬,不同步的现象肯定会出现。在老的标准里还比较少,
在c++11以后经常发现,后续版本会对前面的版本打补丁,其实好多就是因为这种情况。
在前面分析过很多通过完美转发来实现的例子,比如才分析过的单例。在c++11以后,使用完美转发加上右值引用几乎可以用来常见的参数处理和转发控制(当然还是有一些是有问题的)。
但是这里会有一个应用场景出现(总有但是),如果想转发类型内部成员怎么办?类似于下面:
struct Data
{
int d;
};
void getData(int d){}
template
void send(T && t)
{
getData(std::forward(t).d);
}
代码跑起来很正常。但这个代码可不可以扩展呢?比如在Data内部有指针,有自定义类,有STL中的容器?可以试一下,用std::vector或者自己写一个类。程序跑起来仍然没有什么问题,但这就没问题了吗?
在上一节提出了问题,首先回答这个问题要先回到完美转发的目的来。std::forward的目的是在模板编程时能够保持原参数的类型和值类型状态(左或右值等),从而准确的传递参数。这其实在c++非泛型编程中可以窥探出它的效果。举一个简单的例子,有一个函数接收int参数,但传入short也是没有问题的。原因是c++的隐式转换,这种转换很常见,但风险也非常大。最典型的就是有符号类型和无符号类型的转变时的溢出问题。它在非泛型编程中,还是比较容易发现和查找的,但是在泛型编程,也就是模板中,很难发现。同时,在后期的类型萃取中也会导致异常。等等还有其它一些问题,都是完美转发被广泛应用的一个原因。
所以,这时回到原来的问题上,对于普通(基础)类型或者自定义的一些普通类对象,做为右值仍然会被传递成为右值,这个不会有什么问题。但对于指针和容器或者自定义的一些特殊情况的类型,就有问题了。
std::forwardstd::vector中,标准库对[]有两个重载即只区分了常量和非常量。也就是说vector::[]const返回一个左值常量,可move(t)(也就是完美转发)后,move(t).vec[id]仍然是一个左值。它不是一个右值,也就是完美转发的过程中虽然把const传递了下去,但是左右值丢失了。
那么利用forward(t).容器内容,这种情况,c限定符保留了下来,但左右值失去了,那么使用std::forward的目的也失去了。而指针更甚一层,它的处理只是被传回一个非常量的左值(除非原来指针指向就是一个常量)。这样有没有完美转发,意义已经不存在了。
在自定义类中,如果稍微复杂一些,一定可能包含上述的情况,那么,完美转发的意义就大大失色了。c++的大佬们当然不会坐视这个问题,于是提出了std::forward_like:
getData(std::forward_like(t.d));
//如果含有指针:
getData(std::forward_like(*t.ptr));
它的形式和std::forward有一些不同,可以理解成直接完美转发成员了。
在前面分析过Deducing This,std::forward_like可以和这个属性共用,用来转发自己的成员:
template
struct Data
{
T* value;
template
decltype(auto) operator*(this Owner&& owner)
{ return std::forward_like(*owner.value); }
};
再看一个常用的lambda表达式应用:
template
auto check(F&& f) {
return [f = std::forward(f)](this auto&& owner)noexcept(!std::invoke(std::forward_like(f)));
}
这里的就可以从模板参数的起始来完美转发相关的常量和左右值了。此处的noexcept使用到了其对表达式的处理方式,即noexcept(expression),noexcept和noexcept(true)等价,表示不抛出异常,为false时表示可能抛出异常。在新的标准里throw()这种方式已经被抛弃。
再看一个cppreference上的例子:
#include
#include
#include
#include
#include
#include
#include
struct TypeTeller
{
void operator()(this auto&& self)
{
using SelfType = decltype(self);
using UnrefSelfType = std::remove_reference_t;
if constexpr (std::is_lvalue_reference_v)
{
if constexpr (std::is_const_v)
std::cout << "const lvalue\n";
else
std::cout << "mutable lvalue\n";
}
else
{
if constexpr (std::is_const_v)
std::cout << "const rvalue\n";
else
std::cout << "mutable rvalue\n";
}
}
};
struct FarStates
{
std::unique_ptr ptr;
std::optional opt;
std::vector container;
auto&& from_opt(this auto&& self)
{
return std::forward_like(self.opt.value());
// It is OK to use std::forward(self).opt.value(),
// because std::optional provides suitable accessors.
}
auto&& operator[](this auto&& self, std::size_t i)
{
return std::forward_like(container.at(i));
// It is not so good to use std::forward(self)[i], because
// containers do not provide rvalue subscript access, although they could.
}
auto&& from_ptr(this auto&& self)
{
if (!self.ptr)
throw std::bad_optional_access{};
return std::forward_like(*self.ptr);
// It is not good to use *std::forward(self).ptr, because
// std::unique_ptr always dereferences to a non-const lvalue.
}
};
int main()
{
FarStates my_state{
.ptr{std::make_unique()},
.opt{std::in_place, TypeTeller{} },
.container{std::vector(1)},
};
my_state.from_ptr();
my_state.from_opt();
my_state[0]();
std::cout << '\n';
std::as_const(my_state).from_ptr();
std::as_const(my_state).from_opt();
std::as_const(my_state)[0]();
std::cout << '\n';
std::move(my_state).from_ptr();
std::move(my_state).from_opt();
std::move(my_state)[0]();
std::cout << '\n';
std::move(std::as_const(my_state)).from_ptr();
std::move(std::as_const(my_state)).from_opt();
std::move(std::as_const(my_state))[0]();
std::cout << '\n';
}
运行结果:
mutable lvalue
mutable lvalue
mutable lvalue
const lvalue
const lvalue
const lvalue
mutable rvalue
mutable rvalue
mutable rvalue
const rvalue
const rvalue
const rvalue
注意,编译器需要支持。
大家一起来打补丁吧。估计大佬们的心里也是有多少匹马在奔腾,但奔腾向何方,这个只有他们自己知道。快有快的好,慢有慢的好。哪个才是最合适的,只有试试才知道。然后,大佬们说:试试就试试。