重新认识 C++

分享 Modern C++ 的冰山一角的一角

开源软件协会  RC

Outline

  • 热身
  • Lambda 表达式
  • RAII
  • 智能指针
  • 模板

提示

  • 可能需要一些 C++ 基础
  • 后面的内容默认使用 C++17 标准
  • 并不是所有特性都要求 C++17,但这里不做区分
class Foo {
public:
    Foo(int num) : number_(num) {
    }

    int get_number() const {
        return number_;
    }

private:
    int number_ = 0;
};
const float PI = 3.14;

// 除了默认访问说明符之外和 class 没有区别
struct Bar {
    float radius = 0.0;
    float length = 0.0;

    float get_volume() const {
        return PI * radius * radius * length;
    }
};

热身:class 和 struct

void *p1 = 0; // bad!
void *p2 = NULL; // bad! same as (void *)0
void *p3 = nullptr; // good!

热身:new、delete、nullptr

Foo *foo = new Foo(2333);
int num = foo->get_number();
delete foo;
foo = nullptr; // avoid double free

int *arr = new int[233];
arr[232] = 1;
delete[] arr;
using namespace foo::baz::foo;
using foo::bar::A;
using B = foo::bar::B;
using foo::get_the_answer;

struct D : A {
    using A::A;
};

热身:命名空间和 using 声明

namespace foo {
    inline int get_the_answer() {
        return 42;
    }

    namespace bar {
        struct A {};
        class B {};
    } // namespace bar

    namespace baz::foo {
        enum class C { HELLO, WORLD };
    } // namespace baz::foo
} // namespace foo

热身:auto 和 decltype

auto generate_map() {
    std::unordered_map<std::string, std::pair<int, std::tuple<float, float, float>>> map;
    map["foo"] = {1, {1.2, 1.3, 4.5}};
    return map;
}

void modify_map(decltype(generate_map()) &map) {
    map["bar"] = {2, {0.6, 7.8, 9.9}};
}

int main() {
    auto i = 42;
    auto b = true;
    auto vec = std::vector<int>{};
    auto map = generate_map();
    modify_map(map);
}

Lambda 表达式

  • 匿名函数
  • 可赋值给变量或作为函数参数
  • 可捕获外层作用域的变量(闭包)

Lambda 表达式:几个例子

struct Event {
    std::string type, user_id, message;
};

auto is_valid = [](const Event &event) -> bool {
    const static std::set<std::string> types{
        "message",
        "notice",
        "request",
    };
    return types.count(event.type) > 0;
};

auto e1 = Event{"message", "张三", "你好"};
auto e2 = Event{"foo", "小明", ""};
assert(is_valid(e1) && !is_valid(e2));
auto gen_message_event = [] {
    return Event{"message", "", ""};
};
auto e = gen_message_event();
assert(e.type == "message");
std::string s = "Hello, world!";
auto pos = std::find_if(
    s.cbegin(),
    s.cend(),
    [](auto ch) {
        return ch == ',' || ch == '!';
    }
);
assert(*pos == ',');

Lambda 表达式:几个例子

std::string s = "Hello, world!";
auto pos = std::find_if(
    s.cbegin(),
    s.cend(),
    [](auto ch) {
        return ch == ',' || ch == '!';
    }
);
assert(*pos == ',');
enum Direction { TOP = 0, RIGHT, BOTTOM, LEFT, INVALID };
auto direction = TOP;
auto next_direction = [&direction] {
    auto ret = direction;
    direction = static_cast<Direction>(direction + 1);
    if (direction == INVALID) direction = TOP;
    return ret;
};

for (int i = 0; i < 10; i++) {
    std::cout << next_direction() << std::endl;
}

RAII

  • 资源获取即初始化(Resource Acquisition Is Initialization),一种编程风格
  • 资源的生命周期和对象生命周期绑定
  • 构造函数中获取资源、析构函数中释放资源
  • 移动构造函数和移动赋值运算符中转移资源

RAII:在文件流中的应用

int read_n() {
    std::ifstream in("n.txt"); // 创建对象时,在构造函数中获取资源(打开文件)
    int n;
    in >> n;
    return n; // 离开作用域时,在析构函数中释放资源(关闭文件)
}

RAII:在锁中的应用

std::mutex m;

void lock_bad() {
    m.lock();
    // do something
    m.unlock();
}

void lock_good() {
    std::lock_guard<std::mutex> lk(m);
    // do something
}

RAII:在锁中的应用

std::mutex m;

void lock_good_2() {
    // do something
    {
        std::lock_guard<std::mutex> lk(m);
        // do something with lock
    }
    // do something
}

智能指针

  • 自动管理内存
  • 有三种:unique_ptr、shared_ptr、weak_ptr
  • 这里只介绍 shared_ptr

智能指针:Why?

struct Request {
    std::string method;
    std::string path;
    Headers headers;
    std::string body;
};

// 使用传统的 new、delete
auto request = new Request{.method = "POST", .path = "/", .body = "{}"};
// handle the request
delete request; // 如果忘记 delete,或 handle 时抛出异常,则内存泄漏

// 如果 Request 是 RAII 风格的类,遗漏 delete 将导致资源被占用,RAII 的优势瞬间丧失

智能指针:shared_ptr

struct Request {
    std::string method;
    std::string path;
    Headers headers;
    std::string body;
};

// 使用 shared_ptr
auto request = std::shared_ptr<Request>(
    new Request{.method = "POST", .path = "/", .body = "{}"}
);
// handle the request
// 无需手动 delete,当引用计数降为 0 时会自动释放

智能指针:make_shared

struct Response {
    Headers headers;
    std::string body;
};

auto response = std::make_shared<Response>(); // 完全避免使用 new

智能指针:引用计数

{
    std::shared_ptr<int> p = nullptr;
    {
        auto tmp = std::make_shared<int>(42); // tmp 持有指针,引用计数为 1
        *tmp = 233;
        p = tmp; // 指针复制给 p,引用计数为 2
    } // 离开 tmp 的作用域,引用计数为 1
    assert(*p == 233);
} // 离开 p 的作用域,引用计数为 0,销毁对象、回收内存

模板

  • 类模板、函数模板、别名模板、变量模板
  • 这里只介绍类模板和函数模板的最基础用法

模板:类模板

template <typename Elem>
class Vector {
public:
    Vector() : data_(nullptr), size_(0) {}
    Vector(size_t n) { data_ = new Elem[n], size_ = n; }
    ~Vector() { delete[] data_; }
    // ...
private:
    Elem *data_;
    size_t size_;
};

Vector<int> int_vec;
Vector<Vector<double>> double_vec_vec;

模板:函数模板

template <typename T>
T sum(const std::vector<T> &vec) {
    T sum{}; // 零初始化
    for (const auto &item : vec) {
        sum += item; // T 需实现 += 运算
    }
    return sum;
}

auto s1 = sum<double>(std::vector<double>{1, 2.5});
auto s2 = sum(std::vector<int>{111, 222}); // 从参数可以推断出 T 时,无需显式指定

总结

  • C++ 是一门博大精深的语言,不止是「面向对象的 C」
  • 限于时间和我的水平,这里只能介绍一点皮毛
  • 模板相关内容值得深入学习

延伸阅读