Professional Documents
Culture Documents
html
Modern C++
exception
相较于C用返回值提示程序运行状态,C++引入了异常处理机制来处理程序出错的情况。
C语言程序出错处理风格
通过显式地处理每步错误,并返回错误码给上层函数,以通知执行错误。递归如上操作,直到
找到能处理错误的函数。
1 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
int readFile(const char* filepath, char** buffer, int** offsets, int* lineCount)
FILE* fp = fopen(filepath, "r");
if (!fp) { // <---------------------------------------------- check error
return -1; // cannot open file
}
// allocate memory
*buffer = (char*)malloc(byteCount * sizeof(char));
*offsets = (int*)malloc(lineCount * sizeof(int));
*lines = lineCount;
if (*buffer == NULL || *offsets == NULL) {// <--------------- check error
fclose(fp); // <---------------------------------------- release resources
return -10000; // out of memory
}
// record data
char* p = *buffer;
offsets[0] = 0;
for (int i = 0; i < lineCount; ++i) {
if (fgets(p, bytesCount, fp) == NULL) {// <-------------- check error
*lines = i - 1; // record valid lines
fclose(fp); // <------------------------------------- release resources
return -100; // file operation error
}
if (i > 0)
offsets[i] = offsets[i - 1] + strlen(p);
p += lineLengths[i];
}
C++程序出错处理风格
通过异常抛出错误, 只处理当前函数能处理的错误。
2 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
/* @file ./codes/exception/example1.cpp */
std::vector<std::string> readFile(const std::string filepath) {
std::vector<std::string> lines;
std::ifstream fs(filepath, std::ios::in);
try {
fs.exceptions(std::ifstream::failbit | std::ifstream::badbit);
std::string line;
while (getline(fs, line)) {
lines.push_back(line);
}
return lines;
} catch (std::ifstream::failure& e) {
if (!fs.eof()) { // getline will cause failbit at the end of the file
throw std::runtime_error("read " + filepath + " error: " + e.what())
}
} catch (std::bad_alloc& e) {
throw std::runtime_error(std::string("out of memory: ") + e.what());
}
return lines;
}
异常的使用
C++标准库为我们提供了如下的异常,定义在头文件stdexcept中。
这些异常也被用在标准库的其他组件中,如vector, iostream等
我们可以(通过继承标准库中的异常)自定义新的异常,使程序更好地处理不同的错误
3 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
/* @file ./codes/exception/example2.cpp */
class OutOfStock: public std::runtime_error {
public:
explicit OutOfStock(const std::string &s) : std::runtime_error(s) {};
};
noexcept关键字
与异常相关的另一语法是noexcept,用以说明函数不会抛出异常,提高程序可解释度,并帮助编
译器执行对无异常代码的特殊优化。
noexcept修饰的函数一致性问题
noexcept修饰的类成员函数一致性问题
class Base {
public:
virtual void f() noexcept; // will not throw except
virtual void g(); // might throw except
};
值得指出的是编译器并不会在编译时检查noexcept说明,即如果noexcept中抛出异常,是能通
过编译的。
4 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
但运行时,如果抛出异常,程序则会调用terminate以确保遵守不在运行时抛出异常的承诺。
因此noexcept可以被用在如下情况:1.函数不会抛出异常,2.我们确实无法处理异常。
/* @file ./codes/exception/example3.cpp */
void f(void) noexcept {
std::cout << "void f() noexcept" << std::endl;
throw std::runtime_error("throw an except"); // program terminates
}
lambda表达式
lambda表达式是C++11最好用的语法糖之一,匿名函数意味着无需在全局定义函数,只要就地展开
函数体即可。
语法标准
// [ capture ] 捕获列表,可以是如下之一
// [], 空
// [=], lambda所在范围内的所有可见变量,包括lambda表达式所在类的this指针,以值传递
// [&], lambda所在范围内的所有可见变量,包括lambda表达式所在类的this指针,以引用
// [this] lambda所在类的所有成员变量与函数
// [a] 将变量a按值传递,且默认是const(opt被省略时),即lambda表达式内不可修改a的值
// [&a] 将变量a以引用传递
// [a, &b] 将变量a以引用传递, b以引用传递
// [=, &a, &b] 除a, b以引用传递外,其余变量均按值传递
// [&, a, b] 除a, b以值传递传递外,其余变量均按引用传递
// ( params ) 函数参数列表,当没有参数时,可以省略()
// opt 为mutable或exception
// mutable 用以标识值传递的变量是可修改的
// exception 用以表示lambda表达式可能抛出异常
/* @file ./codes/lambda/example1 */
auto add = [](int i, int j){ return i + j; };
add(1, 2); // return 1 + 2
class A {
public:
int call() { return [this] { return add(m_i, m_j); }(); }
private:
int m_i = 1, m_j = 2;
int add(int i, int j) { return i + j; }
};
5 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
lambda表达式调用标准库函数
C++标准库集成了lambda表达式例子
std::vector<int> v(1000);
std::for_each(v.begin(), v.end(), [](int& x) {
x = 0;
});
lambda表达式与外部库函数结合使用
由于越来越多的库,如tbb和cuda,开始支持Modern C++的语言特性,使用lambda表达式能更
简单地调用库函数
thrust::device_vector<uint32_t> indices(objectCount);
thrust::copy(thrust::make_counting_iterator<uint32_t>(0),
thrust::make_counting_iterator<uint32_t>(objectCount), indices.begin());
左值与右值
左值与右值是C++11最重要的概念。右值的引入是为了解决对象多次构造问题。
通过引入右值,编译器就能从右值中窃取资源,而不是重复拷贝。
6 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
左值与右值的概念
在C++中所有表达式都被如下的分类系统归类。
expression
/ \
gvalue rvalue
/ \ / \
lvalue xvalue prvalue
lvalue即左值,表示内存中实际存在的一块持久存储的区域,在表达式结束后依然占据有效内
存。
rvalue即右值,表示表达式结束后就不存在的临时对象。
evalue即消亡值,也就是即将被销毁、却能够被移动的值。
prvalue即纯右值, 如纯粹的字面量,或字面量表达式,Lambda 表达式等。
右值引用
C++ 定义&&符号为右值引用(不包括模板的情况),右值引用可以以右值为初值构造,延续右值的
生命周期。
int&& i = 1;
int&& j = i++;
double&& d = 1.0 + 2.0;
可以利用std::move进行左值到右值的转换。
以下给出std::move源码
7 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
/* @file vcruntime.h */
#define _NODISCARD [[nodiscard]] // attribute
/* @file xtr1common */
template <class _Ty> // generalized
struct remove_reference { using type = _Ty; }; // simplified version
/* @file type_traits */
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept {
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
移动语义
我们可以在类中定义移动构造函数,该函数接受一个右值作为参数,并将资源传递给要构造的
新对象。
多线程
C++正式引入了多线程编程模型,充分利用现代处理器多核性能。
8 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
std::thread
std::vector<std::thread> threads;
threads.push_back(std::move(t));
}
std::mutex 与 std::condition_variable
std::mutex就是我们在操作系统中使用到的互斥锁。
std::mutex mtx;
mtx.lock();
// critical section
mtx.unlock();
std::condition_variable则是比较新的内容,熟悉pthread库的用户应该对此并不陌生。
std::condition_variable与std::mutex配合使用,下面演示使用上述两者创建多线程队列。
9 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
/* @file ./codes/multithreading/example2.cpp */
template <typename T>
class ConcurrentQueue {
public:
void push(T value) {
std::lock_guard<std::mutex> lockGuard(m_mtx);
m_dataQueue.push(std::move(value));
m_cond.notify_one();
}
value = std::move(m_dataQueue.front());
m_dataQueue.pop();
return true;
}
private:
std::mutex m_mtx;
std::condition_variable m_cond;
std::queue<T> m_dataQueue;
};
std::atomic
std::atomic为C++11提供的原子操作库,主要包含std::atomic_flag与std::atomic<T>的模板对
象。
std::atomic_flag使用非常简单,仅有初始化、test_and_set与clear操作,下面我们使用
std::atomic_flag开发一个自旋锁。
/* @file ./codes/multithreading/example3.cpp */
class SpinLock {
public:
void lock() { while (m_flag.test_and_set()); }
void unlock() { m_flag.clear(); }
private:
std::atomic_flag m_flag = ATOMIC_FLAG_INIT;
};
std::atomic<T>提供了我们更多地操作原子变量的机会。
10 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
template<typename T> // exchange the value of atomic variable and return the old value
_NODISCARD T exchange(const T value, const memory_order order) volatile noexcept
在深入了解atomic之前,我们必须引入内存序的概念。
内存序定义了在单线程中的指令重排约束,并定义了多线程间的同步关系。
上面的自旋锁版本中,,可用acquire release语义做进一步优化
/* @file ./codes/multithreading/example3.cpp */
class SpinLock {
public:
void lock() { while (m_flag.test_and_set(std::memory_order_acquire)); }
void unlock() { m_flag.clear(std::memory_order_release); }
private:
std::atomic_flag m_flag = ATOMIC_FLAG_INIT;
};
注意误用memory_order会造成很大问题,在特定的平台上可能是正确的,但不能保证所有平台
都是正确的。
11 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
上述4个线程执行完成后,z的值理论上可能依然为0,这是因为同步关系仅存在两线程之间。
智能指针
内存管理一直是C++令人比较头疼的问题,智能指针则是C++内存管理最佳实践,其本质上是个资
源所有权问题。
如果手动管理内存,在函数的多个出口点,都需要显式地调用释放资源函数来清理内存,这是十分
反人类的。
另外在多线程框架下,不同线程需要共享数据,智能指针确保持有智能指针的线程的数据一定是有
效的。
std::unique_ptr
unique说明其管理的对象是独占的,因而禁止其他智能指针与其共享同一个对象。
12 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
std::unique_ptr基本使用
/* ./codes/smart pointer/example1.cpp */
std::unique_ptr<int> p1(new int(0));
std::unique_ptr<int> p2 = p1; // error, copy constructor is deleted
std::unique_ptr<int> p3 = std::move(p1); // ok, ownership is given to p3
std::unique_ptr做资源管理
/* ./codes/smart pointer/example2.cpp */
void printFile(const char* filepath) {
FILE* fp = fopen(__FILE__, "r");
if (fp == NULL) {
std::cerr << "cannot open file" << std::endl;
return;
}
**std::shared_ptr
std::shared_ptr通过引用计数的方式,记录多少个shared_ptr指向同一对象,当引用计数为0
时,自动删除对象。
/* ./codes/smart pointer/example3.cpp */
void foo(std::shared_ptr<int> i) {
std::cout << "foo: " << i.use_count() << std::endl; // foo: 2
(*i)++;
}
// ...
std::shared_ptr<int> p1 = std::make_shared<int>(10); // value: 10
std::cout << "main: " << p.use_count() << std::endl; // main: 1
foo(p1); // after the expression, the content of p1 would be 11;
std::weak_ptr
为避免循环引用,我们需要引入std::weak_ptr
13 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
struct A {
std::shared_ptr<B> pointer; // must change one of pointer to std::weak_ptr
};
struct B {
std::shared_ptr<A> pointer; // must change one of pointer to std::weak_ptr
};
//...
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->pointer = b;
b->pointer = a; // memory leak
杂项
using关键字
using 定义类型别名
using 引入命名空间与外部类型
using std::vector;
using namespace std;
using 重载父类函数
在子类中使用using声明引入基类成员名称
14 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
class Base {
public:
std::size_t size() const { return n; } // will be private in derived class
protected:
std::size_t n; // will be private in derived class
};
using 定义继承构造
传统C++子类构造函数如果需要继承,则需要一一传递参数,这将导致效率低下。C++11
引入继承构造函数的概念。
/* @file ./codes/miscellaneous/example1.cpp */
class Base {
public:
Base() { value1 = 1; }
Base(int value) : Base() { // delegating constructor
value2 = value;
}
private:
int value1, value2;
};
枚举类
C++11引入了新的限定作用域的枚举类,用以解决之前重名问题。
基本语法
// c++98 style
enum Color {RED, BLUE, GREEN};
enum DressColor {BLUE, ORANGE, GREEN}; // `BLUE` conflicts with a previous declaration
Color color = RED; // ok
int value = RED; // ok
// c++11 style
class enum Color {RED, BLUE, GREEN};
class enum DressColor {BLUE, ORANGE, GREEN}; // ok
Color color = Color::RED; //ok
Color color = RED; // error
int value = Color::RED; // error, even if the RED is int by default, no implicit conver
15 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
定义枚举类的类型
枚举类型变量的默认值为int,但我们可以为枚举类定义不同的类型,以满足我们的实际需
求。
auto关键字
auto优点
使用auto关键字,能大大节约我们的脑力与打字量。编译器通过推导自动获取auto对应的
类型。
std::vector<double> v;
std::pair<std::unordered_multimap<int, int>::iterator,
std::unordered_multimap<int, int>::iterator> range1 = umap.equal_range(key);
auto range2 = umap.equal_range(key); // c++11
auto类型推导
int x = 0;
auto *p1 = &x; // auto->int
auto p2 = &x; // auto->int*
auto &r1 = x // auto->int
auto r2 = r1; // auto->int, & is removed
循环的几种写法
C++11引入了多种循环遍历方法,使得书写循环变得更加简单。
16 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
/* ./codes/miscellaneous/example3.cpp */
std::vector<double> v(2'0000'0000);
列表初始化
为进一步统一C++中对象的初始化语法,C++11引入了列表初始化
语法
int i{1};
double d{1.0};
int a[] = { 1, 2, 3 };
int *p = new int[3]{1, 2, 3};
std::vector<int> v = {1, 2, 3};
class A {
public:
A(int i, int j): v{2} {}
private:
int v;
};
A a{1, 2};
列表初始化解决窄化问题
内存对齐
不同编译器的内存方式不同,这给程序移植造成了一定的困难。
17 of 18 18/12/2020, 3:30 pm
content http://10.214.164.125/yy/cplusplus/content.html
通过alignas关键字,我们可以指定内存对齐的大小,既可以对普通变量的生效,也可以对结构体
生效。
注意alignas(n)中的n只能是2的次方,如1, 2, 4, 8, 16, 32, ...。且对齐结果只大不小。
/* @file ./codes/miscellaneous/example5.cpp */
struct A { // size padding
char c; // 1 3
int i; // 4 0
double d; // 8 0
}; // total 16
另外,在手动分配内存时,我们需要对齐的内存,以支持一些SIMD指令,如Intel的AVX指令集
18 of 18 18/12/2020, 3:30 pm