LOADING

加载过慢请开启缓存 浏览器默认开启

C++ 笔记(2)—— Effective Modern C++ (1-3 章)

2024/5/25 Language C++

以下笔记囊括了前三章我学习到的新知识。

Item 1 —— 模板参数

推导按值传递的模板形式参数类型 T 时,若传入的实参带有 constvolatile 时,将会去掉。
因为按值传递就相当于是对数据进行了拷贝,不应该有上述限制修饰。

若形式参数为 T,传入的数据为函数或者数组,将会退化成对应的指针。除非使用 T &

Item 2 —— auto

一般而言,使用 auto 和模板 T 是一样的。除了以下的情况:

// x 的类别为 std::initializer_list<int>
auto x = {1, 2, 3};

template <typename T>
void func(T x);
// 错误,无法推断 T 的类型
func({1, 2, 3})

// 错误,无法推断类型
// auto 用在函数类型推导时会被当作模板类型推导
auto func() {
    return {1, 2, 3}
}

auto 可以用来推断一些只有编译器清楚的类型,比如 lambda 表达式。

auto x = []() { return "test"; };
auto y = std::function{[]() { return "test"; }};

比起 ystd::function 包裹,使用 auto 的效率更高,内存占用更少。

Item 3 —— decltype

下面的代码将报错:

template <typename Container, typename Index>
auto func(Container& c, Index i) {
    return c[i];
}
std::vector<int> vec{1, 2};
func(vec, 1) = 10;

此处 auto 相当于 T,按值传递。这将会把 vector::operator[] 返回的 T& 的引用给去掉。我们无法给一个右值赋值。

而如下的代码将通过编译:

template <typename Container, typename Index>
decltype(auto) func(Container& c, Index i) {
    return c[i];
}
std::vector<int> vec{1, 2};
func(vec, 1) = 10;

一般而言,不同于 auto 和模板类型推导,decltype 只会鹦鹉学舌。
对于类型为 T 的表达式,除非该表达式仅有一个名字,否则 decltype 总是得出类型 T &

// 此处类型为 int &
decltype(auto) f() {
    int x = 0;
    return (x);
}

Item 5 —— 优先选用 auto 而非显式类别声明

  • 例子 1:vector<int>::size()

    vector<int>::size() 返回的类型是 vector<int>::size_type,用很多常见的显示类别去声明会产生隐式的类型转换,在不同平台可能会有一定问题。

  • 例子 2:遍历 map

    std::map<std::string, int> m;
    for (const std::pair<std::string, int>& p : m) {
    }
    

    这么写看似没有问题,但是实际上真正遍历的时候 p 的类型应该是 std::pair<const std::string, int> &。上述的遍历方式会新建临时变量。这是 map 中的源码:

    typedef pair<const key_type, mapped_type>        value_type;
    ...
    typedef value_type&                              reference;
    

    转换成 auto 之后:

    std::map<std::string, int> m;
    for (const auto& p : m) {
    }
    

    此时 p 的类型为 const std::map::value_type &,符合预期。

  • 例子 3:std::vector<bool>

    std::vector<bool>::operator[] 的返回值不是 bool &,而是 std::vector<bool>::reference。这是因为 bool 在 c++ 里面是用一个 bit 表示的,vector<bool> 不允许直接返回一个 bit 的引用。

    这里如果用 auto 就可能会产生问题,而用 bool 则可以实现隐式类型转换。

Item 7 —— () 与 {}

std::vector<int>{1, 2};
std::vector<int>(1, 2);

两者完全不一样。

只要有机会,总是倾向于使用具有 std::initializer_list 类型的构造函数。

Item 9 —— using 与 typedef

using 支持模板,而 typedef 只能新建一个模板类:

template <typename T>
class Container {
    T x;
};

// 支持
template <typename T>
using ContainerList = std::vector<Container<T>>;
ContainerList<int> list;
// 不支持
template <typename T>
typedef std::vector<Container<T>> ContainerList;

// 支持
template <typename T>
class ContainerList {
   public:
    typedef std::vector<Container<T>> type;
};
typename ContainerList<int>::type list;

这里必须要有 typename,因为有些特例化实现可能会定义一个叫做 type 的成员对象,需要明确告诉编译器这里用的是 type 这个类型。

Item 10 —— 优先使用 enum class

enum class 会限制作用域。普通的 enum 中的枚举值可能会与其他的同名变量产生歧义。而使用 enum class 声明的枚举值只能通过 enum_class_name::value 的方式来访问。

同时,没有 class 限制的枚举值可以发生隐式类型转换,而加了限制的不行:

enum Color1 { red, white };
enum class Color2 { red, white };
// 可以
if (Color1::red > 1.5) {
}
// 不可以
if (Color2::red > 1.5) {
}

enum class 底层使用 int,也可以使用其他类型:

enum class Status: std::uint32_t;

Item 11 —— 使用 delete 而非未定义的私有函数

应该将声明为 delete 的函数声明为 public,这样便于生成更好的报错信息。
假如设置为 private,用户使用了这个函数,一些编译器会先检查函数的可访问性,说该函数是 private,但是更重要的信息显然是该函数被标记为了 delete

Item 12 —— 使用 override 关键字

使用 override 可以让编译器更好地识别函数是否实现了重写。例如 C++ 11 中有一个鲜为人知的特性:

class A {
    void doSomething() &;
    void doSomething() &&;
}

是两个函数,&& 对应右值调用该函数的情况,例如下面的例子:

class A {
   public:
    void doSomething() & { std::cout << "Called first." << std::endl; }
    void doSomething() && { std::cout << "Called second." << std::endl; }
};

A getRVal() {
    return A{};
}

int main() {
    A a{};
    a.doSomething();
    getRVal().doSomething();
}

将输出:

Called first.
Called second.

Item 14 —— noexcept

在保证一定不会产生异常的前提下,能用 noexcept 就用,因为编译器可以对具有该标识的函数进行更好的优化。同时,编译器不会强求 noexcept 一定要调用具有 noexcept 标识的函数。

Item 15 —— constexpr

  • constexpr 对象(包括函数)都具有 const 属性,且由编译期已知的值完成初始化。
  • constexpr 函数在调用的时候,如果传入的实参值是编译器已知的,则会产出编译期结果。否则退化成普通的函数,在运行时计算结果。

好处是可以在编译期就确定一些变量的值,坏处是可能增加编译时间。

Item 16 —— 保证 const 函数的线程安全性

对于被 const 修饰的函数,我们应该保证其线程安全性。例如:

class A {
   private:
    bool is_valid{};
    int val{};
    mutable std::mutex mtx;

   public:
    int getVal() const {
        std::lock_guard<std::mutex> lock_guard(mtx);
        if (is_valid) {
            return val;
        }
        return -1;
    }
};

为了这一点,我们通常会配合 std::mutex 或者 std::atomic 等同步原语。由于 mutex 在上锁的时候会修改其内部状态(该函数未被标记为 const),所以我们需要通过 mutable 告诉 const 函数该变量是会变化的,不应该在函数内限制其不可变。