以下笔记囊括了前三章我学习到的新知识。
Item 1 —— 模板参数
推导按值传递的模板形式参数类型 T
时,若传入的实参带有 const
或 volatile
时,将会去掉。
因为按值传递就相当于是对数据进行了拷贝,不应该有上述限制修饰。
若形式参数为 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"; }};
比起 y
用 std::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
函数该变量是会变化的,不应该在函数内限制其不可变。