其他分享
首页 > 其他分享> > 技术书籍 — EffectiveMordenCpp 研读

技术书籍 — EffectiveMordenCpp 研读

作者:互联网

一、类型推导

PROs:

Cons:

二、move和forward

要点:当传递给函数我右值引用时,应该无条件转换为右值(使用std::move);通用引用应该有条件转换为右值(使用std::forward

举个栗子:

class Widget {
  public:
    // rhs 为一个右值引用,使用std::move
  	Widget(Widget&& rhs): name(std::move(rhs.name)), p(std::move(rhs.p)) {}
  
    template<typename T>
    void setName(T&& newName){ // newName是一个通用引用
      name = std::forward<T>(newName); // 使用std::forward
    }
  private:
  	std::string name;
    std::shared_ptr<Data> p;
};

在上述栗子中,通用引用可能绑定到有资格移动的对象上。当使用右值初始化时,std::forward才会将其强制转化为右值

扩展场景一:

在使用按值返回的函数,且返回值绑定到右值引用或通用引用,需要对返回值使用std::movestd::forward

// Case 1: std::move
// 左侧的参数也用于保存计算的结果
Matrix operator+(Matrix&& lhs, const Matrix& rhs){
  lhs += rhs;
  return std::move(lhs); //lhs 可以移动到返回值时的内存位置
  // return lhs; 会被编译器copy到返回值的内存空间
}

// Case2: std::forward
template<typename T>
Fraction reduceAndCopy(T&& frac){
  frac.reduce();
  /*
   *1. 如果 frac 是一个右值,则会直接移动到返回值中,避免copy开销
   *2. 如果 frac 是一个左值,则必须创建副本
   */
  return std::forward<T>(frac);
  // return frac; // 总是会创建副本
}

扩展场景二:

开发者可能会误用 std::move,比如下面的栗子。

Widget makeWidget(){
  Widget w;
  ...;
  return w; // 编译器会自动进行RVO(返回值优化)
  //return std::move(w); // 不要这样做!
}

RVO(返回值优化)的条件:

  1. 局部变量与返回值的类型相同
  2. 局部变量就是返回值
/*
 * 1. 此处返回的已经不是局部对象 w,而是w的引用
 * 2. 返回 w 的引用不符合RVO的第二个条件
 * 3. 试图帮助编译器优化,反而限制了优化效果
 */
return std::move(w);

三、熟悉通用引用重载的替代方法

方式一:Pass by Value

将按引用传递参数替换为按值传递(这违反直觉)。

class Person{
  public:
     // 替换原生的 T&&
     explicit Person(std::string s): name(std::move(s)){}
     explicit Person(int idx): name(nameFromIdx(idx)){}
  private:
  	 std::string name;
};

方式二:使用Tag dispatch

首先回顾一下存在重载问题的case代码:

std::multiset<std::string> names;
template<typename T>
void logAndAdd(T&& name)
{
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  // 当 name 是一个int类型的重载,则会导致names.emplace(int)错误
  names.emplace(std::forward<T>(name));
}

解决方案:

template<typename T>
void logAndAdd(T&& name)
{
  // 借助一个 bool tag实现重载函数的分发
  logAndAddImpl(std::forward<T>(name),
               std::is_integeral<typename std::remove_reference<T>::type>());
  // 不直接使用std::is_integeral<T>()的原因是:is_inteageral<T&>会返回FALSE,对应传入左值的情况
}

template<typename T>
void logAndAddImpl(T&& name, std::false_type)
{
  names.emplace(std::forward<T>(name));
}

// 特化版本实现
std::string nameFromIdx(int idx);
void logAndAdd(int idx, std::true_type)
{
  logAndAdd(nameFromIdx(idx));
}

方法三:约束使用通用引用模板

要点:搭配使用 std::enable_ifstd::decaystd::is_base_of

class Person
{
  public:
  // 模板应该在 T 不是Person的时候启用
    template<typename T, typename=std::enable_if<!std::is_base_of<Person, std::decay<T>>::value>>
    explicit Person(T&& p);
};

使用 std::enable_if来选择性禁止Person通用引用构造器,以使得一些参数确保使用移动或copy构造函数。

四、引用折叠

引用折叠发生在四种情况:

Widget w;
// w1为一个左值引用,auto推导出来的为 Widget&
// 代入即为:Widget& && w1 = w; 引用折叠后为 Widget& w1 = w;
auto&& w1 = w; 

通用引用不是一种新的引用,它实际上是满足两个条件下的右值引用:

typedef的样例:

template<typename T>
class Widget
{
  public:
  	typedef T&& RvalueRefToT;
};

Widget<int&> w;
/*推导-->*/ typedef int& && RvalueRefToT;
/*引用折叠-->*/ typedef int& RvalueRefToT;

这清楚地表明 typedef 有时可能并不按照我们预期的设置生效的。

五、完美转发的失败case

std::forward只适用于通用引用场景,在按值传递和指针参数并不适用。

如下是常用的场景:

template<typename T>
void fwd(T&& param)
{
  f(std::forward<T>(param));
}

template<typename... Ts>
void fwd(Ts&&... params)
{
  f(std::forward<T>(param)...);
}

失败case一:Braced initializers(支撑初始化器)

void f(const std::vector<int>& v);

f({1,2,3});// ok, 隐式转换
fwd({1,2,3}); // compile error;
// 原因:推导传入fwd的参数类型,将其与f的声明类型比较

auto il = {1, 2, 3};
fwd(il); // ok

失败case二:0或者NULL作为空指针

0或者NULL作为空指针给模板时,类型推导只会推导出整型,而不是一个指针类型。建议使用nullptr代替

失败case三:仅声明的整数静态const数据成员

class Widget
{
  public:
    static const std::size_t MinVals = 28;
};
std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals); //ok

f(Widget::MinVals);//ok
fwd(Widget::MinVals);//可以编译,但link error

const std::size_t Widget::MinVals; // 加上定义即可

六、智能指针

要点一:unique_ptr

// TODO

标签:std,const,研读,int,param,name,EffectiveMordenCpp,书籍,引用
来源: https://www.cnblogs.com/CocoML/p/14643409.html