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

参考

这是 Back to the Basics: Essentials of Modern C++ 的视频总结。

要点

1. 使用 range 进行迭代

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

没有特别需要说明的。

2. 避免使用 new 和 delete

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

#include 
#include 
#include 

namespace c14 {
template 
std::unique_ptr make_unique(Args &&... args) {
  return std::unique_ptr(new T(std::forward(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("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(); 
...
f( *upw );

auto spw = make_shared();
...
g( spw.get() );

auto 关键字

主要讲两点。

  1. 关于性能
auto x = value;

这个语句是否创建了一个 value 然后通过 copy/move 的方式给 x 呢?
并没有。实际上以下两句是等同的。

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

那么形如:

auto x = type{value};

这个语句是否创建了一个临时对象并且通过 copy/move 转移给 x 呢?
答案是肯定的,但是编译器可能会优化。并且仍然需要保证这个临时对象是 copyable/movable 的。

  1. 关于不能使用 auto 的地方
    在使用上面第二种方式初始化时,auto 不适用于无法 copy/move 或者 copy/move 代价昂贵的对象。
auto lg = lock_guard{mu};  // error, not movable
auto ai = atomic{0};  // error, not movable
auto a = array{};  // compiles, but needlessly expensive

4. 右值优化

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

#include 
#include 

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。例子如下,注意是使用了 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;
  }
  1. forwarding reference 用于编写 forwarder,可以在保留参数性质(左值、右值、const)的情况下传递参数。
namespace c14 {
template 
std::unique_ptr make_unique(Args &&... args) {
  return std::unique_ptr(new T(std::forward(args)...));
}

6. 使用 tuple 返回多个值

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

#include 
#include 
#include 

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

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

  // C98
  pair::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::iterator iter;
  bool success;
  tie(iter, success) = aSet.insert(str3);
  if (success == true) {
    cout << "Inserted: " << *iter << endl;
  }
  return 0;
}

你可能感兴趣的:(返璞归真:现代C++精要)