返璞归真:现代C++精要

来自:找不到工作

参考

这是 Back to the Basics: Essentials of Modern C++ 的视频总结https://v.qq.com/x/page/y0324m1wgji.html

要点

1、使用 range 进行迭代

for (auto& e : c) {...}

没有特别需要说明的。

2.、避免使用 new 和 delete

在需要使用 new 创建对象的场合,使用 unique_ptr 代替。例如:

#include <iostream>
#include <string>
#include <utility>

namespace c14 {
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args &&... args) {
  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// namespace c14

class Person {
public:
  explicit Person(const std::string &name, int age) {
    name_ = name;
    age_ = age;
    std::cout << "Created with lvalue" << std::endl;
  }

  explicit Person(const std::string &&name, int age) {
    name_ = std::move(name);
    age_ = age;
    std::cout << "Created with rvalue" << std::endl;
  }

  void speak() {
    std::cout << "Hello, I'm " << name_ << ", " << age_ << " years old."
              << std::endl;
  }

  ~Person() { std::cout << name_ << " deleted..." << std::endl; }

private:
  std::string name_;
  int age_;
};

int main(int argc, char const *argv[]) {
  auto p = c14::make_unique<Person>("yy"27);
  p->speak();
  return 0;
}

/* 输出:
Created with rvalue
Hello, I'm yy, 27 years old.
yy deleted...
*/

尽量使用 unique_ptr 来管理对象的所有权,当确定某个对象需要共享则使用 shared_ptr。

1、它的开销和裸指针一样
2、它是 exception-safe 的,即使发生异常也能顺利析构
3、无需进行麻烦的引用计数

以上例子有两个值得注意的地方。

1、std::make_unique 是 C++14 引入的特性。但是我们这里为了方便 C++11 用户自己手动实现了一个。实际也不复杂,主要是变长参数模板的处理,以及 std::forward 实现的完美参数转发。相比 std::move, std::forward 会进行引用折叠。
2、区分左值与右值。

3、在传值的时候仍然应该使用 * 以及 &

这是最安全而且性能最好的。

不要使用智能指针作参数,无论是 by value 还是 by reference,除非你想在函数里控制对象的生命周期。

无论是 copy 还是 assign 一个 shared_ptr,都会影响引用计数,改变对象的生命周期。
比较理想的做法应该是传递引用或裸指针。

例如:

auto upw = make_unique<widget>(); 
...
f( *upw );

auto spw = make_shared<widget>();
...
g( spw.get() );

auto 关键字

主要讲两点。

1、关于性能

auto x = value;

这个语句是否创建了一个 value 然后通过 copy/move 的方式给 x 呢?

并没有。实际上以下两句是等同的。

T x = a;  // 1
x(a);  // 2

那么形如:

auto x = type{value};

这个语句是否创建了一个临时对象并且通过 copy/move 转移给 x 呢?

答案是肯定的,但是编译器可能会优化。并且仍然需要保证这个临时对象是 copyable/movable 的。

2、关于不能使用 auto 的地方

在使用上面第二种方式初始化时,auto 不适用于无法 copy/move 或者 copy/move 代价昂贵的对象。

auto lg = lock_guard<mutex>{mu};  // error, not movable
auto ai = atomic<int>{0};  // error, not movable
auto a = array<int50>{};  // compiles, but needlessly expensive


4、右值优化

在参数传递中,可以恰当地使用右值优化。

#include <string>
#include <iostream>

class Employee {
public:
  // 1
  void set_name(const std::string& name) {
    name_ = name;
    std::cout << "set name with lvalue" << std::endl;
  }
  // 2
  void set_name(std::string&& name) noexcept {
    name_ = std::move(name);
    std::cout << "set name with rvalue" << std::endl;
  }
  void speak() {std::cout<< "My name is " << name_ << std::endl;}
private:
  std::string name_;
};

int main(int argc, char const *argv[]) {
  Employee e;
  std::string s = "ssss";
  std::string b = "bbbb";
  e.set_name(s);
  e.speak();
  e.set_name(s+b);
  e.speak();
  return 0;
}

值得注意的是 noexcept 这个关键字。由于函数 1 有可能会发生内存分配(例如传递一个右值作为参数),因此是可能发生诸如内存不足等异常的。而函数 2 是不可能的。这种设计体现了 exception-safety。

但是,对于构造函数,建议使用传值的方式。原因可以参考 stackoverflow https://stackoverflow.com/questions/4321305/best-form-for-constructors-pass-by-value-or-reference。例子如下,注意是使用了 std::move。

class Employee {
public:
  Employee() {}
  //  for constructor, pass by value is a good idea
  explicit Employee(std::string name): name_(std::move(name)) {

  }
private:
  std::string name_;

5.、正确使用 &&

&& 不仅表示 rvalue reference,还可以表示 forwarding reference。

两者的使用场景不同。

1、rvalue reference 用于右值优化,例如上面的:

  // 1
  void set_name(const std::string& name) {
    name_ = name;
    std::cout << "set name with lvalue" << std::endl;
  }
  // 2
  void set_name(std::string&& name) noexcept {
    name_ = std::move(name);
    std::cout << "set name with rvalue" << std::endl;
  }

2、forwarding reference 用于编写 forwarder,可以在保留参数性质(左值、右值、const)的情况下传递参数。

namespace c14 {
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args &&... args) {
  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}


6、使用 tuple 返回多个值

这里介绍了三种方式,个人推荐第三种,更清晰一点。

#include <set>
#include <tuple>
#include <iostream>

using namespace std;
int main(int argc, char const *argv[]) {
  set<string> aSet{"Hello""World""SB"};

  string str1 = "Hello";
  string str2 = "You";
  string str3 = "Me";

  // C98
  pair<set<string>::iterator, bool> result1 = aSet.insert(str1);
  if (result1.second == true) {
    cout << "Inserted: " << *result1.first << endl;
  } else {
    cout << "Insert " << str1 << " failed." << endl;
  }

  // C11: auto
  auto result2 = aSet.insert(str2);
  if (result2.second == true) {
    cout << "Inserted: " << *result2.first << endl;
  }

  // C11: tie
  set<string>::iterator iter;
  bool success;
  tie(iter, success) = aSet.insert(str3);
  if (success == true) {
    cout << "Inserted: " << *iter << endl;
  }
  return 0;
}
推荐↓↓↓
C语言与C++编程
上一篇:Effective C++ 读书笔记 下一篇:GDB调试指南-启动调试