现在是 9102 年了,再来总结 C++ 的惯用法似乎有点落伍,但是 C++ 的强大依然毋庸置疑。最近我也把目光投向 rust,C++ 不管怎么演进,历史包袱还是太沉重了。而且,rust 的设计其实也参考了很多C++的思想(如 RAII)所以在 C++ 中继续深入也是有价值的
内容大纲
- Assertion
- Marcos
- SFINAE
- Meyers Singleton
- RAII
- Type Trait
0. Assertion
运行期断言
1
2
3
4
5
|
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif
|
assert 其实是在来源于C语言的头文件 中定义的宏
一般来说,如果condition 是false,那么assert 首先向stderr 打印错误信息,然后调用 abort (触发一个SIGABRT中断信号)来非正常结束程序
- assert 不是 error handling, assertion 处理的是代码的 bug 而不是程序预期的输入错误或者系统错误等
- 想象有这样一个函数,下面哪种实现更”安全“呢?:
1
2
3
4
5
6
7
8
9
10
|
void foo1(int score) {
if(score > 0 && score <= 100){
// internal logics
}
}
void foo2(int score) {
// require bar between 1 and 100
assert(score > 0 && score <= 100 && "score must between 1 and 100!");
}
|
让错误尽早发现(Fail Fast[1])
- 一个 fail fast 的系统通常倾向于直接报告错误给上层逻辑。
- 此类设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到故障。
assert 优点
- 帮助程序员做好模块化
- 在内部逻辑执行之前提供可行性检查
- 造成程序的 fast fail
- 在 release 模式下会被 compiled out, 不影响性能
缺点
- 只在 debug 模式下工作,如果在 assert 中写有副作用的逻辑,会造成 release 版本的严重错误
- 使得程序的内在逻辑对外部部分可见
编译期断言
编译期断言对应 static_assert,是 C++ 11 提供的让错误更早发现的机制。
static_assert 十分强大,附带的自定义信息会直接被当成编译错误输出,但由于是编译期断言,因此限制也很大。他的输入只能是bool_constexpr类型。因此要用好static assert,需要程序员分辨出代码哪些部分是编译期即可确定的部分。
下面的代码可以限制 Vector 的 dimension 始终大于 1
1
2
3
4
5
6
7
8
|
template<typename ElemType, int N>
class Vector {
static_assert(N > 1, "1D Vector is not allowed, use scalar types");
ElemType elem_[N];
};
Vector<int, 1> vec1;// compilation error
Vector<int, 2> vec2;
|
另外,利用 static_assert + type_traits 也能实现语义约束,比如我们想写一个检查无符号加法溢出的函数,并希望调用方不传递有符号类型,那么我们可以:
1
2
3
4
5
6
|
template<typename T>
bool check_overflow_add_unsigned(T a, T b) {
static_assert(std::is_unsigned<T>::value, "T must be unsigned types!");
...
...
}
|
由于自 C++ 11 开始,鼓励大家多用编译期计算,把一些 constant 的数据和代码段尽量挪到编译期,因此可能很多类型成为 literal type (比如上面的 Vector )。这些类既能在运行期运行,又能在编译期预计算,因此,只要是涉及 literal type 的函数/类/对象 就应尽可能的用 static_assert
1. Marcos
这里说的宏是指 replacing text macros,是由预处理器支持的文本替换功能,同样支持带参数的文本替换
宏是许多编程语言都支持的功能,C/C++ 中的宏是较为简单的文本替换,而在 rust 和 lisp 等语言中,宏会直接展开AST,因此更为强大,既有类型系统支持,又能做到zero cost abstraction
尽管如此,在C/C++中 使用宏很多时候能大大简化代码,在一些情况下能代替C/C++中比较啰嗦的逻辑。例如当我们想在每个类的定义插入特定的信息用来做反射,以及当我们想做一个循环展开以便更好的利用SIMD的性能,宏都能给我们提供这样的能力,但有时候也会给开发者带来困惑,尤其多层嵌套的宏在可读性和维护性上比较差。但目前,宏在C/C++ 中的地位仍然是不可取代的
开发中,我们常用下面几种宏的写法
单行宏
1
2
3
|
#define a 1
#define b (a + 1)
#define c a * b
|
多行宏
1
2
3
|
#define Vec \
static int x; \
static int y;
|
多行宏会在在使用处被替换为多行内容
宏参数
1
2
3
4
5
6
7
8
9
10
11
|
#include <iostream>
#define DUPLICATE(index, times) for(int index = 0; index < times; ++index)
int main(void)
{
DUPLICATE(i, 100)
{
std::cout << i << std::endl;
}
return 0;
}
|
# 和 ## 操作符 (预处理器)
Stringizing operator (#)
在类函数的宏中 # + 形参名可以在宏替换中 直接把参数字面量换成字符串
例:
1
2
3
4
5
6
7
8
9
|
#define DEFINE_AND_PRINT(type, varname) \
type varname; \
std::cout << "declare " << #type << " " << #varname << std::endl;
int main(int argc, char const *argv[])
{
DEFINE_AND_PRINT(int, bar); // declare int bar
return 0;
}
|
Token-Pasting Operator(##)
## 直接把前后的字符合并成一个token,可以用来自动生成出一些新的token(类名,函数名,变量名等)
1
2
3
4
5
|
#define CREATE_3_VARS(name) name##1, name##2, name##3
int CREATE_3_VARS(myInts);
myInts1 = 13;
myInts2 = 19;
myInts3 = 77;
|
其他注意
- 由于宏本质上是预处理器对代码做的文本替换,因此一般需要一到两个括号包裹来指定优先级。
- 注意宏替换发生在整个编译器起作用之前,即早于所谓的词法分析之前
2. Meyer’s Singleton
我们怎样在C#中实现单例?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public sealed class Singleton
{
static Singleton instance = null;
public void Show()
{
Console.WriteLine( "instance function");
}
private Singleton()
{
}
public static Singleton Instance
{
get
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
|
线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public sealed class Singleton
{
static Singleton instance = null;
private static readonly object padlock = new object();
private Singleton()
{
}
public static Singleton Instance
{
get
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
}
}
return instance;
}
}
}
|
Double-Check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public sealed class Singleton
{
static Singleton instance = null;
private static readonly object padlock = new object();
private Singleton()
{
}
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}
|
Meyers’ Singleton in C++
1
2
3
4
5
|
static Singleton& instance()
{
static Singleton s;
return s;
}
|
(Scott Meyers 是Effective C++系列的作者)
相对于C#的单例实现,Meyers提供了一种相当简洁的书写方式
Meyers 单例利用了C++11 的 Dynamic Initialization and Destruction with Concurrency[2] 特性,该特性要求编译器对于每一个静态对象都绑定一个哨兵变量(guard variable)用来做线程相关的操作,保证 static local variable 在并发条件下只执行一次Lazy Initialization。
以clang 为例,Meyers 单例生成的汇编代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
Singleton::instance(): # @Singleton::instance()
push rbp
mov rbp, rsp
sub rsp, 16
cmp byte ptr [guard variable for Singleton::instance()::singleton], 0
jne .LBB1_4
movabs rdi, offset guard variable for Singleton::instance()::singleton
call __cxa_guard_acquire
cmp eax, 0
je .LBB1_4
mov eax, offset Singleton::instance()::singleton
mov edi, eax
call Singleton::Singleton() [base object constructor]
jmp .LBB1_3
.LBB1_3:
movabs rdi, offset guard variable for Singleton::instance()::singleton
call __cxa_guard_release
.LBB1_4:
movabs rax, offset Singleton::instance()::singleton
add rsp, 16
pop rbp
ret
mov ecx, edx
mov qword ptr [rbp - 8], rax
mov dword ptr [rbp - 12], ecx
movabs rdi, offset guard variable for Singleton::instance()::singleton
call __cxa_guard_abort
mov rdi, qword ptr [rbp - 8]
call _Unwind_Resume
Singleton::Singleton() [base object constructor]: # @Singleton::Singleton() [base object constructor]
push rbp
mov rbp, rsp
mov qword ptr [rbp - 8], rdi
mov rdi, qword ptr [rbp - 8]
mov dword ptr [rdi], 0
pop rbp
ret
Singleton::instance()::singleton:
.zero 4
guard variable for Singleton::instance()::singleton:
.quad 0 # 0x0
|
3.SFINAE(subtitition failure is not an error) [3]
类型 SFINAE
注意:如果有条件使用 static_assert 或者 concepts,尽量不要使用 SFINAE
我们经常要根据模板类型参数的不同来展开不同的代码,这这个工作有时候能利用特化完成,但也有特化无法完成的时候,比如我们想实现overload function template,就像普通函数的重载一样,这时候就需要SFINAE的帮助
SFNIAE 是C++模板技术的衍生,属于模板元编程的一部分
Subtitition Failure
在C++ 中,模板参数的subtitition(替换) 发生在模板展开的过程中。参数替换有两步:
- 首先发生显式指定的替换
- 其次发生类型推断的替换,以及默认模板参数的替换
所谓默认模板参数很好理解,和函数的默认参数一样,例子如下:
1
|
template<typename T, typename U = int> // int is the default template parameter for argument U
|
当不指定 U 时,U默认为int
由于替换的次序不同,在替换默认模板参数的时候,实际上可以利用已经替换过的参数来替换默认参数
1
|
template<typename T, typename U = typename T::internal_type>
|
而替换失败(subtitition failure) 指的是上述的替换产生了问题,比如传递给类型参数A的类型可能没有一个叫internal_type的嵌套类型,
然而,要使编译器产生compilation error,还需要尝试有没有别的函数模板能替换成功,这个过程可能发生多次subtitition failure。
有人说那我改一下默认参数是不是能产生subtitition failure is not an error呢?很遗憾不行。只有default template parameter 不同的template是有歧义的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <iostream>
struct A {
using internal_type = int;
};
struct B {};
template <typename T, typename U = typename T::internal_type> void foo() {
std::cout << "T has internal_type" << std::endl;
}
template <typename T> void foo() {
std::cout << "T has no internal_typ" << std::endl;
}
int main(int argc, char const *argv[]) {
foo<A>(); // call to 'foo' is ambiguous
foo<B>();
return 0;
}
|
那么到底如何产生 subtitition failure is not an error呢?
A simple and explicit Example
Example from wikipedia
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
struct Test {
typedef int foo;
};
template <typename T>
void f(typename T::foo) {} // Definition #1
template <typename T>
void f(T) {} // Definition #2
int main() {
f<Test>(10); // Call #1.
f<int>(10); // Call #2. Without error (even though there is no int::foo) thanks to SFINAE.
}
|
Typedef detector
这是 SFINAE 的一个应用,用来检查类参数有没有 T 有在类内部 typedef internal_type 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include <iostream>
struct A {
using internal_type = int;
};
template <typename T>
decltype(std::declval<typename T::internal_type>(), std::declval<void>())
foo(const T &) { // compare to the varadic version below, this one is more specifc
std::cout << "T has internal_type" << std::endl;
}
void foo(...) { std::cout << "T has no internal_type" << std::endl; }
// template <typename T> void foo() { foo(int()); }
int main(int argc, char const *argv[]) {
A a;
foo(a); // subtitition suceeded for the first time
foo(1); // subtitition failed, but succeeded for the second time
foo("test"); // subtitition failed, but succeeded for the second time
return 0;
}
|
我们看到foo有两个重载版本,为了告诉编译器我期望 T 有嵌套 internal_type 类型,我们使用了 decltype + 逗号表达式 + declval。decltype(std::declval(), std::declval()) 的真正类型是 void,但对T::internal_type进行了求值,因此编译器知道这里 T 是 more specific 的
member functor detector
更有用的例子是判断类型有没有包含某个名字的成员函数,这一点对template programming 中的duck typing 很重要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#include <iostream>
#include <string>
#include <type_traits>
template <class T>
struct Stringlizable
{
// We test if the type has serialize using decltype and declval.
template <typename C>
static constexpr decltype(std::declval<C>().serielize(), bool())
test(int /* unused */)
{
// We can return values, thanks to constexpr instead of playing with sizeof.
return true;
}
template <typename C>
static constexpr bool test(...) { return false; }
// int is used to give the precedence!
static constexpr bool value = test<T>(int());
};
struct A
{
int data;
std::string to_string() { return std::to_string(data); };
};
struct B
{
};
template <typename T>
std::string Serielize(T obj) { return obj.to_string(); }
int main(int argc, char const *argv[])
{
A a;
B b;
std::cout << Serielize(a) << std::endl;
return 0;
}
|
表达式 SFINAE (略)
4. Type Traits
终于来到了 type traits。
上文中曾提到,static_assert 以及 SFINAE 和 type_traits 结合可以在编译器做一些类型约束的事情,,那么这个type_traits到底是什么呢
要理解Type Traits,我们首先要理解C++的类型系统
C++是一门静态类型语言,其类型系统十分强大,由三个部分组成:基本类型,RTTI, Type Traits
Type traits 定义了一套编译期的,基于模板的用来查询或修改类型属性的接口
cppreference [4]中列出了这些接口,它们是以 helper class 来实现的
一个简单的例子是上文提到的 match unsigned 类型的泛型函数:
1
2
3
4
5
6
|
template<typename T>
bool check_overflow_add_unsigned(T a, T b) {
static_assert(std::is_unsigned<T>::value, "T must be unsigned types!");
...
...
}
|
现在我们终于可以尝试解释一下这个 std::is_unsigned::value 是个什么东西了
cppreference 中给出的可能实现如下:
1
2
3
4
5
6
7
8
9
10
|
namespace detail {
template<typename T,bool = std::is_arithmetic<T>::value>
struct is_unsigned : std::integral_constant<bool, T(0) < T(-1)> {};
template<typename T>
struct is_unsigned<T,false> : std::false_type {};
} // namespace detail
template<typename T>
struct is_unsigned : detail::is_unsigned<T>::type {};
|
首先,is_unsigned 是一个struct,其有一个static constexpr bool member 叫 value,value 继承自 std::integral_constant<bool, T(0) < T(-1)> 或者 false_type,是一个编译期计算的量。
我们从上面的代码中可以再次发现了 SFNIAE 的存在,编译器首先替换继承自 std::integral_constant 的版本,如果这个替换是invalid 的(可能是无法显式 从 -1 构造,或者编译期构造出来的 -1 > 0),那么将考虑更加generic 的版本,这个版本 的 is_unsigned 继承自 false_type ,这样value 就继承到了constexpr false
除了 is_unsigned, cppreference 中还有非常多的helper_class, 能帮助我们写出更加 generic 的代码
(注意,尝试特化<type_traits> 中的template是未定义行为)
5. RAII(Resource acquisition is initialization)[5]
RAII 可能是C++在某种程度上优于其他基于GC的语言最主要的原因
简单来说,C++提供了确定性析构过程,尤其对于栈对象,异常,多返回分支语句,多线程环境C++都能保证在栈上分配的对象一定会被清理,而清理之前一定会调用对象的析构函数。
同时,我们通常会在栈上创建很多临时资源,如网络连接,文件描述符,申请堆内存等。这些临时资源是需要被清理的。手动去清理他们通常会考虑不周全,通过和临时对象的析构绑定,我们可以确保资源被释放。
最典型的例子其实来自于 C++ STL
RAII 在 STL 中被大量使用。文件流是一个很好的例子。销毁文件流对象,也会同步关闭文件,而不需要手动关闭文件描述符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include <mutex>
#include <iostream>
#include <string>
#include <fstream>
#include <stdexcept>
void write_to_file (const std::string & message) {
// mutex to protect file access (shared across threads)
static std::mutex mutex;
// lock mutex before accessing file
std::lock_guard<std::mutex> lock(mutex);
// try to open file
std::ofstream file("example.txt");
if (!file.is_open())
throw std::runtime_error("unable to open file");
// write message to file
file << message << std::endl;
// file will be closed 1st when leaving scope (regardless of exception)
// mutex will be unlocked 2nd (from lock destructor) when leaving
// scope (regardless of exception)
}
|