Skip to content

Commit 09ba13e

Browse files
committed
Added utils-tips.md and cpp-coding-style.md to docs/
Some tips on Bash and Vim. Some tips on C++ coding.
1 parent b9b7995 commit 09ba13e

File tree

2 files changed

+459
-0
lines changed

2 files changed

+459
-0
lines changed

docs/cpp-coding-style.md

+329
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
# Almost Always Auto
2+
3+
`auto` might make others be a little confused about the deduced types, but it has more advantages:
4+
* It saves keyboard strikings.
5+
* It make the variable initialization mandatory.
6+
* It requires less overhead while refactoring.
7+
8+
9+
# Always Use `override` on Inherited Method
10+
11+
By doing this way, if you happen to write the wrong signature, the compiler would warn you. Otherwise, say that you use `virtual` instead, you are declaring a new virtual method, but not a inherited one.
12+
13+
14+
# Almost Never Use `new` and `delete` Explicitly
15+
16+
Principals to choose between `std::shared_ptr`, `std::unique_ptr` and raw pointer:
17+
* Whenever you deal with some resource, you MUST sort out its ownership, thoroughly. If not, dont't touch it. If can't, ask the author.
18+
* Use `std::unique_ptr`
19+
* When you allocate and use a resource locally.
20+
* When you own the resource or pass a resource around, and nobody else needs to keep, use or share it.
21+
* Use `std::shared_ptr`
22+
* When multiple places need to own a resource at the same time, and the owners have loose dependencies on each other.
23+
* Use raw pointers
24+
* You could keep a raw pointer to some resource, only if the resource is owned by some other resource(s) and it would stay availble during the whole time you holding it.
25+
* We hope we could accomplish the goal that whenever one sees a raw pointer, he has no need to concern about the ownership of the resource. Once not able to, add comments about the ownership wherever it appears.
26+
27+
Remember, remember, all is about ownership.
28+
29+
30+
# What does the accociated lock of conditional variable protect?
31+
32+
Usually, the lock could be used to protect the related data structures from race condition, but it's not designed to. Actually, it protects the **condition**, to be precise, the **condition changing**. For example, if the condition is simply a boolean value or some inherently thread safe data(as we all know, that write or read on a boolean value on x86_64 platform is atomical). This may tempt us to not acquire the lock while we changing it, but this couldn't be more wrong.
33+
34+
BTW. another common mistake we may make while using conditional variable is that, we forgot to check the condition before we fall asleep via `wait`, which will lead us to miss the notify event(actually, we already *missed* it, since it arrived before we acquire the lock). Another pitfall is that we must check on the condition after we are woken up, to prevent the spurious wakeup.
35+
36+
Here are some bad cases:
37+
```c++
38+
AtomicData data;
39+
std::mutex lock;
40+
std::conditional_variable cond;
41+
42+
void thread1() {
43+
std::unique_lock<std::mutex> guard(lock);
44+
cond.wait(guard, [&]() { return data.ready(); });
45+
}
46+
47+
void thread2() {
48+
{
49+
// Here we change the condition without the `lock' held.
50+
// If the condition were changed after thread1 had checked on it,
51+
// and the following notification arrives before thread1 falls asleep,
52+
// thread1 will never be woken up.
53+
54+
// Otherwise, if we change the condition with the lock held,
55+
// only two cases could happen while we changing the condition:
56+
// 1. thread1 is waiting on the lock. In this case, after we release the lock,
57+
// thread1 will see it, no need to wait or to be woken.
58+
// 2. thread1 has already fallen into sleep(and the lock has been released),
59+
// then the following notification would wake it up.
60+
data.modifySafely();
61+
}
62+
cond.notify_one();
63+
}
64+
```
65+
66+
```c++
67+
AtomicData data;
68+
std::mutex lock;
69+
std::conditional_variable cond;
70+
71+
void thread1() {
72+
std::unique_lock<std::mutex> guard(lock);
73+
if (!data.ready()) {
74+
cond.wait(guard); // when `wait' returns, the condition might not ready.
75+
}
76+
}
77+
78+
void thread2() {
79+
{
80+
std::unique_lock<std::mutex> guard(lock);
81+
data.modifySafely();
82+
}
83+
cond.notify_one();
84+
}
85+
```
86+
87+
```c++
88+
AtomicData data;
89+
std::mutex lock;
90+
std::conditional_variable cond;
91+
92+
void thread1() {
93+
std::unique_lock<std::mutex> guard(lock);
94+
cond.wait(guard); // this is the silliest mistake.
95+
}
96+
97+
void thread2() {
98+
{
99+
std::unique_lock<std::mutex> guard(lock);
100+
data.modifySafely();
101+
}
102+
cond.notify_one();
103+
}
104+
```
105+
106+
Here is the right way:
107+
108+
```c++
109+
AtomicData data;
110+
std::mutex lock;
111+
std::conditional_variable cond;
112+
113+
void thread1() {
114+
std::unique_lock<std::mutex> guard(lock);
115+
while (!data.ready()) {
116+
// this loop is exactly the same to cond.wait(guard, [&]() { return data.ready(); });
117+
cond.wait(guard);
118+
}
119+
}
120+
121+
void thread2() {
122+
{
123+
std::unique_lock<std::mutex> guard(lock);
124+
data.modifySafely();
125+
}
126+
cond.notify_one(); // whether the notification is protected by lock or not does not make much difference.
127+
}
128+
```
129+
130+
# Prefer `using` to `typedef`
131+
132+
The reason to always use `using`, aka. type alias, is simple: `using` is a superset over `typedef`, besides, `using` is more readable, and most importantly, it could be templated.
133+
134+
135+
# Try Hard to Avoid Defining Global Type Aliases
136+
137+
Of course, type alias saves a lot of keyboard strikings and line breaks for us, thus we encourage to use type aliases. But it also introduces problems:
138+
* It may pollute the naming space.
139+
* It is hard and annoying for others to reason about the real type.
140+
141+
So our suggestions are:
142+
* Prefer defining type alias in block scopes to in function scopes, to in class scopes, to in file scopes, to in the global scope.
143+
* We even prefer giving one type a alias in every compile unit to defining one globally.
144+
145+
146+
# Either Declare a Class `final` or Make the Destructor `virtual`
147+
148+
This is a convention, just comply with it.
149+
150+
151+
# Use `const` Whenever Possible
152+
153+
* For a parameter of type of reference or pointer , make it `const` whenever it is not being modified.
154+
* For a readonly member function, make it `const`.
155+
156+
157+
# Pass By `const &` or By Value?
158+
159+
While defining a function, you might have to decide whether to pass parameters by `const &` or by value. Here are the suggestions:
160+
* When an argument is used in a readonly way, without making a copy from it, use `const &`.
161+
* When you are about to copy from the argument, AND move is not cheaper than copy, such as some aggregation structures, use `const &`.
162+
* Like above, but move is cheaper than copy, pass it by value and always move from it inside the function, thus the caller could choose whether to make the copy or not.
163+
164+
165+
# Don't Make a Return Type `const` If It's By Value
166+
167+
It's non-movable and makes nothing better.
168+
169+
170+
# Qualify the Move Constructor and Assignment with `noexcept`
171+
172+
If you want your class to benefit from the move semantics, and your class are being used in STL containers like `std::vector`.
173+
This is because some STL containers utilize `std::move_if_noexcept` under the hood. If the elements' move constructor or assignment are not qualified with `noexcept`, the movement would be decayed to copy in some actions, such as expansion of `std::vector`.
174+
175+
You could verify this behaviour with the following example:
176+
177+
```c++
178+
class X {
179+
public:
180+
X() {}
181+
X(X&&) NOEXCEPT { fprintf(stderr, "Move\n"); }
182+
X(const X&) { fprintf(stderr, "Copy\n"); }
183+
};
184+
185+
int main() {
186+
std::vector<X> v(4);
187+
v.emplace_back();
188+
return 0;
189+
}
190+
```
191+
```bash
192+
$ g++ -std=c++11 main.cpp -DNOEXCEPT=
193+
$ ./a.out
194+
Copy
195+
Copy
196+
Copy
197+
Copy
198+
$ g++ -std=c++11 main.cpp -DNOEXCEPT=noexcept
199+
$ ./a.out
200+
Move
201+
Move
202+
Move
203+
Move
204+
```
205+
206+
Also, since `std::move_if_noexcept` returns a reference of type `T&&` if `T` has a `noexcept` move constructor, or returns `const T&` if not, you could inspect a type through the compiler error messages of following:
207+
```c++
208+
template <typename> class X;
209+
int main() {
210+
std::string s;
211+
std::function<void()> f;
212+
X<decltype<std::move_if_noexcept(T)> x;
213+
return 0;
214+
}
215+
```
216+
217+
```bash
218+
$ g++ -std=c++11 main.cpp -DT=s
219+
In function 'int main()':
220+
error: aggregate 'X<std::basic_string<char>&&> x' has incomplete type and cannot be defined
221+
X<decltype(std::move_if_noexcept(s))> x;
222+
$ g++ -std=c++11 main.cpp -DT=f
223+
In function 'int main()':
224+
error: aggregate 'X<const std::function<void()>&> x' has incomplete type and cannot be defined
225+
X<decltype(std::move_if_noexcept(f))> x;
226+
```
227+
228+
The above example tells that the move constructor of `std::function` is not qualified `noexcept` movable.
229+
230+
231+
# What's `volatile`?
232+
233+
* It has nothing to do with threading.
234+
* It only affects the code generation of compiler, i.e. issue a memory fetching instruction every time you read the qualified variable, instead of possibly caching it in registers.
235+
236+
237+
```c
238+
//~ add.c
239+
#ifndef VOLATILE
240+
#define VOLATILE
241+
#endif
242+
VOLATILE long a = 0;
243+
VOLATILE long b = 1;
244+
245+
void add() {
246+
a += 1;
247+
b += 1;
248+
a += 1;
249+
b += 1;
250+
}
251+
```
252+
253+
```bash
254+
$ cc -O2 -S add.c
255+
$ cat add.s
256+
```
257+
```asm
258+
add:
259+
movq b(%rip), %rax
260+
addq $2, a(%rip)
261+
addq $2, %rax
262+
movq %rax, b(%rip)
263+
ret
264+
```
265+
266+
```bash
267+
$ cc -DVOLATILE=volatile -O2 -S add.c
268+
$ cat add.s
269+
```
270+
```asm
271+
add:
272+
movq a(%rip), %rax
273+
addq $1, %rax
274+
movq %rax, a(%rip)
275+
movq b(%rip), %rax
276+
addq $1, %rax
277+
movq %rax, b(%rip)
278+
movq a(%rip), %rax
279+
addq $1, %rax
280+
movq %rax, a(%rip)
281+
movq b(%rip), %rax
282+
addq $1, %rax
283+
movq %rax, b(%rip)
284+
ret
285+
```
286+
287+
# Use `do {} while(false)` instead of `goto`
288+
289+
`goto` is good at error handling, and if used with care, it often makes the code more readable than piles of `if`s. But that is the case in C, as for C++, since we are apt to declare objects right before they are actually used and `goto` is forbidden to jump across the initialization point, so we suggest to never use `goto`.
290+
291+
As a workground, a `do while(false)` loop can somewhat do the same job. Follow is a showcase:
292+
293+
```cpp
294+
if (a) {
295+
...
296+
if (b) {
297+
...
298+
if (c) {
299+
...
300+
if (d) {
301+
...
302+
}
303+
}
304+
}
305+
}
306+
do {
307+
if (!a) {
308+
break;
309+
}
310+
...
311+
if (!b) {
312+
break;
313+
}
314+
...
315+
if (!c) {
316+
break;
317+
}
318+
...
319+
if (!d) {
320+
break;
321+
}
322+
...
323+
} while (false);
324+
```
325+
326+
As a supplement, we encourage you to try to make every single line of code deliberate:
327+
* Try to make the pair of `if` `else` blocks reside in a single view.
328+
* Try not to introduce too many indentations, i.e. nesting levels.
329+
* Try to avoid the needs to break one logical line into multiple. (e.g. long names, function call with too many arguemnts)

0 commit comments

Comments
 (0)