Google C++ 风格指南记录
最近在看谷歌的 C++ 风格指南发现了一些有意思的知识点,遂记录下
1. 第六章第二小节介绍了右值引用
只在定义移动构造函数与移动赋值操作时使用右值引用. 不要使用 std::forward
.
定义:
右值引用是一种只能绑定到临时对象的引用的一种, 其语法与传统的引用语法相似. 例如,void f(string&& s)
; 声明了一个其参数是一个字符串的右值引用的函数.
优点:
用于定义移动构造函数 (使用类的右值引用进行构造的函数) 使得移动一个值而非拷贝之成为可能. 例如, 如果
v1
是一个vector<string>
, 则auto v2(std::move(v1))
将很可能不再进行大量的数据复制而只是简单地进行指针操作, 在某些情况下这将带来大幅度的性能提升.右值引用使得编写通用的函数封装来转发其参数到另外一个函数成为可能, 无论其参数是否是临时对象都能正常工作.
右值引用能实现可移动但不可拷贝的类型, 这一特性对那些在拷贝方面没有实际需求, 但有时又需要将它们作为函数参数传递或塞入容器的类型很有用.
要高效率地使用某些标准库类型, 例如
std::unique_ptr
,std::move
是必需的.
缺点:
右值引用是一个相对比较新的特性 (由 C++11 引入), 它尚未被广泛理解. 类似引用崩溃, 移动构造函数的自动推导这样的规则都是很复杂的.
结论:
只在定义移动构造函数与移动赋值操作时使用右值引用, 不要使用std::forward
功能函数. 你可能会使用std::move
来表示将值从一个对象移动而不是复制到另一个对象.
答:std::forward
用于完美转发参数,确保将参数传递给对象的构造函数时保持其原始的左值/右值属性#include <iostream> #include <utility> #include <vector> class MyObject { public: MyObject(int value) : value(value) { std::cout << "Constructor: " << value << std::endl; } private: int value; }; class MyContainer { public: // emplace 函数用于构造元素 template <typename... Args> void emplace(Args&&... args) { // 使用 std::forward 完美转发参数到构造函数 objects.emplace_back(std::forward<Args>(args)...); } private: std::vector<MyObject> objects; }; int main() { MyContainer container; int value = 42; // 使用 emplace 构造 MyObject 对象 container.emplace(value); }
std::emplace_back
可以在容器内部直接构造对象,避免了额外的拷贝和移动操作,提高了性能。
使用 std::emplace_back 需要注意下面的一些事项:
-
理解参数:
std::emplace_back
允许你在容器的末尾构造元素,而不是传递一个已构造的对象。因此,要确保你提供的参数与元素的构造函数参数匹配,以便正确地构造对象。 -
避免不必要的拷贝和移动: 使用
std::emplace_back
时,不会发生拷贝或移动操作,因为元素是直接在容器内部构造的。这可以提高性能,但也意味着你不应该传递一个已经构造好的对象,而是提供构造对象所需的参数。 -
注意引用折叠: 当使用
std::forward
进行参数的完美转发时,要注意引用折叠的情况。确保传递参数时保持原始的左值/右值属性。 -
异常安全性: 在
std::vector
等动态容器中,std::emplace_back
可能会触发重新分配内存。要确保在重新分配内存时不会导致资源泄漏或对象的不一致状态。推荐使用RAII(Resource Acquisition Is Initialization)等技术来保证异常安全性。 -
构造函数的异常: 如果元素的构造函数抛出异常,容器会保持原状,不会插入新元素。确保你的构造函数在异常发生时不会引起资源泄漏,并正确处理异常情况。
-
避免迭代器失效: 在插入元素时,要注意可能会导致迭代器失效,因为容器可能会重新分配内存。如果需要保存迭代器,请在插入操作之前保留或更新迭代器。
-
移动语义的使用: 如果参数为右值引用,确保你在移动构造时正确地使用
std::move
。要遵循移动语义的原则,确保源对象在移动后处于有效但未定义的状态。 -
了解容器的特性: 不同的容器(如
std::vector
、std::list
、std::deque
等)可能有不同的行为,例如动态分配内存的频率和方式。了解容器的特性有助于更好地使用std::emplace_back
。
- 平时怎么格式化函数调用, 就怎么格式化 列表初始化.
- 构造函数初始化列表放在同一行或按四格缩进并排多行.
列表初始化示例:
// 一行列表初始化示范. return {foo, bar}; functioncall({foo, bar}); pair<int, int> p{foo, bar}; // 当不得不断行时. SomeFunction( {"assume a zero-length name before {"}, // 假设在 { 前有长度为零的名字. some_other_function_parameter); SomeType variable{ some, other, values, {"assume a zero-length name before {"}, // 假设在 { 前有长度为零的名字. SomeOtherType{ "Very long string requiring the surrounding breaks.", // 非常长的字符串, 前后都需要断行. some, other values}, SomeOtherType{"Slightly shorter string", // 稍短的字符串. some, other, values}}; SomeType variable{ "This is too long to fit all in one line"}; // 字符串过长, 因此无法放在同一行. MyType m = { // 注意了, 您可以在 { 前断行. superlongvariablename1, superlongvariablename2, {short, interior, list}, {interiorwrappinglist, interiorwrappinglist2}};
构造函数初始化列表示例:
// 如果所有变量能放在同一行: MyClass::MyClass(int var) : some_var_(var) { DoSomething(); } // 如果不能放在同一行, // 必须置于冒号后, 并缩进 4 个空格 MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) { DoSomething(); } // 如果初始化列表需要置于多行, 将每一个成员放在单独的一行 // 并逐行对齐 MyClass::MyClass(int var) : some_var_(var), // 4 space indent some_other_var_(var + 1) { // lined up DoSomething(); } // 右大括号 } 可以和左大括号 { 放在同一行 // 如果这样做合适的话 MyClass::MyClass(int var) : some_var_(var) {}
另外说下 {} 和 () 的使用场合
-
初始化歧义:
- 大括号
{}
初始化也会调用构造函数,但它在某些情况下会更加严格,会对可能的初始化歧义提出更高的要求,特别是在初始化列表和聚合类型上。 - 小括号
()
初始化也调用构造函数,但在一些情况下可能更容易引发初始化的歧义。
- 大括号
-
类型安全性:
- 大括号
{}
初始化通常更严格,不允许窄化转换,这可以提高类型安全性。 - 小括号
()
初始化可能允许某些窄化转换,可能会降低类型安全性。
- 大括号
#include <iostream> class MyClass { public: explicit MyClass(int value) { std::cout << "Constructor: " << value << std::endl; } }; int main() { MyClass obj1(42); // 使用小括号初始化,显示调用构造函数 MyClass obj2{42}; // 使用大括号初始化,会调用构造函数,可能会对初始化歧义提出更高要求 int x(5.5); // 使用小括号,发生窄化转换,x的值为5 int y{5.5}; // 使用大括号,编译器会报错,不允许窄化转换 return 0; }
总而言之,初始化构造函数时可以使用小括号,而列表初始化尽量使用大括号,以避免歧义
3. 所有头文件都应该有 #define 保护来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_ .
概述:为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 foo
中的头文件 foo/src/bar/baz.h
可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_
头文件保护通过预处理器的条件编译(Conditional Compilation)来实现,具体原理如下:
-
当第一次包含头文件时,预处理器会检查该头文件中的宏是否已经定义。由于在开始时没有定义这个宏,条件
#ifndef
判断为真,预处理器会进入条件编译区块。 -
在进入条件编译区块后,宏
PROJECT_PATH_FILE_H_
会被定义,这样就防止了多重包含。 -
当其他源文件也试图包含同一个头文件时,预处理器会再次检查宏是否已经定义。由于在前面的包含中已经定义了
PROJECT_PATH_FILE_H_
宏,条件#ifndef
判断为假,预处理器会忽略后续的头文件内容。
这种方式有效地避免了头文件的多重包含,因为在第一次包含时,宏被定义并且后续的包含都会被忽略。这样做的好处是:
-
避免重复定义: 多重包含可能导致重复定义的错误,而头文件保护可以确保每个头文件在一个编译单元中只包含一次。
-
提高编译速度: 如果没有头文件保护,重复包含会导致编译器不断重复解析同一个头文件,降低了编译速度。
-
避免依赖问题: 有些头文件可能定义了类型或者常量,多重包含可能导致不一致的定义,从而引发链接错误。
综上所述,头文件保护是一种重要的 C++ 编程实践,它确保了头文件的一致性、编译效率和链接正确性。