13. 异常处理

错误处理

异常处理是程序设计中用于处理错误的关键机制,它允许程序在遇到错误时,将错误信息从检测点传递到处理点。异常处理主要依赖于两个核心概念:异常安全保障和资源获取即初始化(RAII)。异常安全保障确保程序能够从运行时错误中快速恢复,而 RAII 技术则通过构造函数和析构函数管理资源,确保资源的正确分配和释放。

异常

异常是一个被程序抛出的对象,代表程序中出现的错误。异常可以是任何可拷贝的对象,但推荐使用自定义类型以避免不同库之间的冲突。异常处理涉及两个主要部分:抛出异常和捕获异常。

抛出异常

当函数无法处理某个问题时,它会抛出一个异常。例如,do_task() ​函数在无法完成任务时抛出 Some_error ​异常:

int do_task() {
    // 如果能够执行该任务
    return result;
    // 否则抛出异常
} else {
    throw Some_error{};
}

捕获异常

调用者可以通过 try ​块和 catch ​从句来捕获并处理异常。例如,taskmaster() ​函数尝试执行 do_task()​,并准备处理 Some_error ​异常:

void taskmaster() {
    try {
        auto result = do_task(); // 使用result
    } catch (Some_error) { // 执行do_task时发生错误:处理该问题
    }
}

异常处理的优点

  1. 改进传统技术:异常处理是对传统错误处理技术的改进,它更高效、精细,减少了错误风险。
  2. 完备性:异常处理能够处理普通代码中发生的各类问题。
  3. 代码分离:允许程序员显式地将错误处理代码从普通代码中分离,提高程序的可读性和可维护性。
  4. 规范性:提供了一种规范的错误处理机制,使得多个独立编写的程序片段更容易合作。

异常的携带信息

异常可以携带关于错误的描述信息。异常的类型表示错误的种类,而异常携带的数据则记录了错误出现时的情形。例如,标准库中的异常可能包含一个字符串,指示抛出异常的位置。

如果你不喜欢为每种错误定义一个专门的类,标准库提供了一个小型的异常类层次供使用。

传统错误处理方法

1. 终止程序

这是一种极端的错误处理方式,通常通过调用 exit() ​或 abort() ​函数来实现。这种方法的问题在于,它不提供错误恢复的机会,也不记录错误信息,直接导致程序终止。

例子:

if (something_wrong) {
    exit(1); // 直接终止程序
}

2. 返回错误值

这种方法通过返回特定的错误值来指示错误发生。然而,它的问题在于,有时候没有合适的“错误值”可用,或者每次调用函数时都需要检查返回值,增加了程序的复杂性。

例子:

int get_int(); // 从输入中获得下一个整数
int value = get_int();
if (value == ERROR_CODE) {
    // 处理错误
}

3. 返回合法值,程序处于错误状态

在这种方法中,函数返回一个合法值,但程序实际上已经处于错误状态。这通常通过设置一个全局或非局部变量(如 C 语言库中的 errno​)来实现。问题在于,主调函数可能没有意识到程序已经处于错误状态,而且跟踪和检测这些错误状态变量增加了程序的复杂性。

例子:

double d = sqrt(-1.0); // -1.0 对于平方根函数是无效的参数
if (errno == EDOM) {
    // 处理错误,errno 被设置表示错误
}

4. 调用错误处理函数

这种方法通过调用一个错误处理函数来处理错误。问题在于,这并没有真正解决问题,只是将问题转移给了错误处理函数。如果错误处理函数不能解决问题,那么它最终还是会面临终止程序、返回错误值、设置错误状态或抛出异常的选择。

例子:

if (something_wrong) {
    something_handler(); // 调用错误处理函数
}

异常处理机制基于渐进决策的思想,即程序在遇到未处理的错误(未捕获的异常)时,最终会终止程序。

异常保障

如果在通过抛出异常终止某个操作后,程序仍然处于有效状态,则称这个操作是异常安全(exception-safe) 的操作。

所谓有效状态,对于一个对象来说,就是保持类的不变式。

此外,还必须释放之前申请的全部资源。

资源管理

推荐使用 RAII 的方式来清理资源。

finally

C++ 不支持异常处理中的 finally ​块。可使用 RAII 的形式,定义一个 finally ​块,从而实现类似的效果。

#include <iostream>
#include <exception>

class Finally {
public:
    Finally(std::function<void()> onExit) : onExit_(onExit) {}
    ~Finally() {
        if (onExit_) {
            onExit_();
        }
    }

    // 防止复制和赋值
    Finally(const Finally&) = delete;
    Finally& operator=(const Finally&) = delete;

private:
    std::function<void()> onExit_;
};

void exampleFunction() {
    try {
        std::cout << "Doing some work..." << std::endl;
        // 模拟资源分配
        int* data = new int[10];

        // 使用RAII来确保资源释放
        Finally finallyBlock([=] {
            delete[] data;
            std::cout << "Resource cleaned up" << std::endl;
        });

        // 模拟可能抛出异常的操作
        throw std::runtime_error("An error occurred");

    } catch (const std::exception& e) {
        std::cout << "Caught an exception: " << e.what() << std::endl;
    }
}

int main() {
    try {
        exampleFunction();
    } catch (...) {
        std::cout << "Caught an unknown exception" << std::endl;
    }
    return 0;
}

抛出与捕获异常

控制流

#include <iostream>
int main() {
	try {
		std::cout << "try" << std::endl;
		throw 1;
	}
	catch (int i) {
		std::cout << "catch" << std::endl;
	}
	std::cout << "end" << std::endl;
}

运行结果

try
catch
end

抛出异常

可以使用 throw ​关键字抛出任意类型的异常,只要该类型是可以被复制或移动的。

class No_copy {
    No_copy(const No_copy&) = delete; // 禁止复制
};

class My_error {};

void f(int n) {
    switch (n) {
    case 0: throw My_error(); // 抛出My_error类型的对象
    case 1: throw No_copy(); // 错误:不允许复制No_copy
    case 2: throw My_error;   // 错误:My_error是一种类型,而非一个对象
    }
}

异常被捕获时,会从抛出点开始“向上”传递,直到找到一个合适的异常处理程序。这个过程称为栈展开(stack unwinding)。在栈展开过程中,所有已经构造的对象都会按照与构造时相反的顺序被销毁。

异常在被捕获前可能会被拷贝多次,因此通常不会在异常中存放大规模的数据。异常传播从语义上类似于初始化,如果抛出的是含有移动语义类型的对象(如 std::string​),则代价会显得不那么昂贵。

C++ 标准库定义了一个异常类型层次体系,可以直接使用,也可以作为基类。标准库异常类接受一个字符串作为构造函数的参数,并用虚函数 what() ​返回该字符串。

#include <stdexcept>
struct My_error2 : std::runtime_error {
	My_error2() : std::runtime_error("My_error2") {}
    const char* what() const noexcept { return "My_error2"; }
};

void f(int n) {
    if (n) {
        throw std::runtime_error("I give up!");
    }
    else {
		throw My_error2();
    }
}

noexcept 函数

noexcept ​关键字用于声明一个函数保证不会抛出异常。这样的声明不仅有助于程序员理解程序逻辑,还能让编译器进行更有效的优化。当函数被声明为 noexcept ​时,程序员不需要为这个函数编写 try ​块来处理可能的异常,编译器也不需要为异常处理准备额外的控制路径。

编译器和链接器并不完全检查 noexcept ​声明的真实性。如果程序员在 noexcept ​函数中抛出了异常,但该异常在函数结束前没有被捕获,程序将直接终止。

如果在 noexcept ​函数内部抛出了异常,且该异常没有被捕获,程序将调用 std::terminate() ​函数无条件终止执行。在这个过程中,程序不会执行任何析构函数,包括主调者的析构函数。这意味着程序的行为是未定义的,我们不能依赖于任何具体对象的行为。

noexcept 运算符

noexcept ​运算符可以用来声明函数在特定条件下不会抛出异常。这可以是无条件的,也可以是基于某个谓词的条件判断。

template<typename T>
void my_fct(T& x) noexcept(is_pod<T>() == true);

noexcept() ​运算符在编译时评估一个表达式,如果编译器“知道”该表达式不会抛出异常,则返回 true​,否则返回 false​。例如:

template<typename T>
void call_f(vector<T>& v) noexcept(noexcept(f(v[0]))){
    for(auto& x : v) f(x);
}

在这个例子中,noexcept(f(v[0])) ​检查函数 f ​调用 v[0] ​时是否会抛出异常。如果不会,则整个 call_f ​函数声明为不会抛出异常。

noexcept 的注意事项

  • noexcept() ​运算符不会对表达式求值,因此即使传给 call_f() ​的是一个空的 vector​,也不会在运行时出错。
  • noexcept(expr) ​运算符不会深入检查 expr ​中的每个操作是否会抛出异常,它只是检查每个操作是否有 noexcept ​说明。

异常说明

在旧版本的 C++ 中,异常说明用于指定函数可能抛出的异常类型。这种说明有两种形式:空异常说明和非空异常说明。

空异常说明 throw() ​与 noexcept ​等价,表示函数不允许抛出任何异常。如果函数抛出了异常,程序将终止。例子:

void g(int) throw(); // 函数g不允许抛出任何异常

非空异常说明,如 throw(Bad, Worse)​,指定函数只能抛出 Bad ​或 Worse ​类型的异常。如果函数抛出了未提及的异常或者不能由参数项公有派生的异常,程序将调用不可预期的异常处理程序(unexpected handler)。默认情况下,这会导致程序终止。例子:

void f(int) throw(Bad, Worse); // 函数f只能抛出Bad或Worse类型的异常

非空异常说明难以使用,因为它们需要在运行时检查抛出的异常是否符合规定,这会带来额外的性能开销。此外,这项功能并不完备,因此建议不要使用。

如果你需要动态地检查抛出的是哪种异常,应该使用 try ​块而不是依赖于异常说明。

捕获异常

void f() {
	try {
		throw E{}
	}
	catch (H) {
		// 何时到达此处?
	}
}

当满足下述条件之一时,系统会调用异常处理程序:

  1. 如果 H 与 E 的类型相同;
  2. 如果 H 是 E 的无歧义的公有基类;
  3. 如果 H 和 E 都是指针类型,并且它们所指的类型满足 1 或者 2;
  4. 如果 H 是引用类型,并且它所引用的类型满足 1 或者 2。

可以在捕获异常的类型前加上 const​,这类似于函数参数的用法。这样做不会改变捕获的异常集合,只是确保我们不会修改异常。

异常类型常常作为类层次的一部分,以反映它们表示的错误之间的关系。

try ​块和 catch ​从句中的作用域是实际的作用域。因此,如果想在 try ​语句的两个部分使用同一个名字,或者想在 try ​块的外部使用某个名字,都必须把该名字声明在 try ​块的外部。

void g() {
    int x1;
    try {
        int x2 = x1;
    }
    catch (int) {
        ++x1; // OK,x1 在 try 块外部声明
        ++x2; // 错误:x2 不在作用域范围内
        int x3 = 7;
        // ...
    }
    catch (...) { // 捕获所有类型的异常
        ++x3; // 错误:x3 不在作用域范围内
    }
    ++x1; // OK,x1 在 try 块外部声明
    ++x2; // 错误:x2 不在作用域范围内
    ++x3; // 错误:x3 不在作用域范围内
}

重新抛出

当一个异常处理程序发现自己无法完全处理捕获的异常时,它可以完成一些局部任务,然后重新抛出该异常。

使用不带参数的 throw ​语句来重新抛出当前异常。这种重新抛出可以发生在 catch ​块中,也可以发生在 catch ​块调用的任何函数中。

如果在没有异常的情况下使用不带参数的 throw​,程序将调用 std::terminate()​。

重新抛出的异常是原始捕获的异常对象,而不是它的任何子对象。这意味着异常的类型和值都会被保留。

多异常处理

一个 try ​块可以跟随多个 catch ​从句,每个 catch ​从句可以捕获不同类型的异常。异常处理程序的书写顺序非常重要,因为程序会按照它们出现的顺序尝试匹配并处理异常。

#include <iostream>
#include <exception>
#include <typeinfo>

void f() {
    try {
        // 可能抛出多种异常的代码
        throw std::bad_cast();
    }
    catch (const std::bad_cast) {
		std::cout << "Caught a std::bad_cast exception" << std::endl;
    }
    catch (const std::exception& e) {
        std::cout << "Caught a standard library exception: " << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception" << std::endl;
    }
}

int main() {
    f(); // 调用f(),将抛出std::bad_cast异常
    return 0;
}

编译器了解类层次的情况,因此它可以发现并报告很多逻辑错误。

void f() {
    try {
        // 可能抛出多种异常的代码
        throw std::bad_cast();
    }
    catch (...) {
        std::cout << "Caught an unknown exception" << std::endl;
    }
    catch (const std::exception& e) {
        std::cout << "Caught a standard library exception: " << e.what() << std::endl;
    }
    catch (const std::bad_cast) {
		std::cout << "Caught a std::bad_cast exception" << std::endl;
    }
}

上面的代码 ...​ 会遮蔽掉下面的所有异常捕获,并且编译器可以捕获到这个错误。

函数 try 块

函数体可以是一个 try 块,这包括普通函数、构造函数、析构函数等。使用函数 try 块通常是为了捕获和处理在函数执行过程中抛出的异常。

int main()
try {

}
catch (...) {

}

在构造函数中使用 try 块可以捕获基类或成员初始化器抛出的异常。默认情况下,这些异常会传递到调用构造函数的地方。通过在构造函数中使用 try 块,我们可以捕获这些异常并进行处理。

#include <vector>
#include <string>
#include <exception>
#include <iostream>

class X {
    std::vector<int> vi;
    std::vector<std::string> vs;

public:
    X(int sz1, int sz2) 
    try 
        : vi(sz1), vs(sz2) 
    {
        // 构造函数体中的代码
    }
    catch (const std::exception& err) {
        // 捕获vi和vs构造过程中抛出的异常
        std::cerr << "Exception in constructor: " << err.what() << std::endl;
        throw; // 重新抛出异常
    }
};

在构造函数和析构函数的 catch 从句中,最应该做的事情是抛出异常。默认操作是在到达 catch 从句的尾端时重新抛出原始异常。

终止

std::terminate() ​会在以下情况下被触发:

  • 没有合适的处理程序可以处理已抛出的异常。
  • noexcept ​函数结束时仍然有未处理的 throw​。
  • 栈展开期间的析构函数结束时仍然有未处理的 throw​。
  • 传播异常的代码(如拷贝构造函数)结束时仍然有未处理的 throw​。
  • 有人试图在没有异常处理的情况下重新抛出一个异常(throw;​)。
  • 静态分配的或线程局部的对象的析构函数结束时仍然有未处理的 throw​。
  • 静态分配的或线程局部的对象的初始化器结束时仍然有未处理的 throw​。
  • 作为 atexit() ​函数调用的函数结束时仍然有未处理的 throw​。

默认情况下,std::terminate() ​会调用 abort()​,导致程序非正常退出。用户可以通过调用 std::set_terminate() ​提供一个自定义的终止处理程序。

如果你试图在同一时刻令两个异常都处于活跃状态(在同一线程中,该用法被禁止),那么系统就不知道该处理哪个异常了:是你刚刚抛出的异常,还是它已经准备处理的异常?请注意,我们一旦进入到 catch 从句中,就表示准备处理异常了。重新抛出异常(见 13.5.2.1 节)和在 catch 从句中抛出一个新异常都被认为是原来的异常被处理之后的新的抛出动作。你也可以在析构函数中抛出异常(甚至在栈展开期间),前提是在离开析构函数之前必须捕获它。

异常与线程

如果一个异常在线程中未被捕获,系统将调用 std::terminate()​,导致整个程序终止执行。为了避免这种情况,必须在线程中捕获所有异常。

可以使用标准库函数 std::current_exception() ​将一个线程中的异常传递给另一线程的处理程序。

#include <iostream>
#include <thread>
#include <future>
#include <exception>

void thread_function(std::promise<std::exception_ptr>& prom) {
    try {
        // 执行某些可能抛出异常的操作
        throw std::runtime_error("Exception in thread");
    }
    catch (...) {
        // 捕获所有异常,并将其传递给future
        std::cout << "Exception caught in thread, passing to main thread." << std::endl;
        prom.set_value(std::current_exception());
        return;
    }
}

int main() {
    std::promise<std::exception_ptr> prom;
    std::future<std::exception_ptr> fut = prom.get_future();
    std::thread t(thread_function, std::ref(prom));

    // 在主线程中等待异常
    std::exception_ptr ptr = fut.get();
    if (ptr) {
        try {
            std::rethrow_exception(ptr);
        }
        catch (const std::exception& e) {
            std::cout << "Exception in main thread: " << e.what() << std::endl;
        }
    }

    t.join();
    return 0;
}

vector 的实现

一个简单的 vector

vector 的简单声明:

#include <memory> // 包含 std::allocator

std::allocator<int> alloc; // 创建一个 int 类型
template<class T, class A = std::allocator<T>>
class vector {
private:
	T* elem; // 分配空间的开始
	T* space; // 元素序列末尾,可扩展空间的开始
	T* last; // 分配空间的末尾
	A alloc; // 分配器
public:
	using size_type = unsigned int; // 表示 vector 尺寸的数据类型

	explicit vector(size_type n, const T& val = T(), const A & = A());

	vector(const vector& a); // 拷贝构造函数
	vector& operator=(const vector& a); // 拷贝赋值运算符

	vector(vector&& a); // 移动构造函数
	vector& operator=(vector&& a); // 移动赋值运算符

	~vector(); // 析构函数

	size_type size() const { return space - elem; } // 返回 vector 的尺寸
	size_type capacity() const { return last - elem; } // 返回 vector 的容量
	void reserve(size_type n); // 增加空间到 n

	void resize(size_type n, const T & = {}); // 改变 vector 的尺寸

	void push_back(const T&);
};

先看一个构造函数的简单实现:

template<class T, class A>
vector<T, A>::vector(size_type n, const T& val, const A& a)
	: alloc{ a }
{
	elem = alloc.allocate(n); // 分配空间
	space = last = elem + n; 
	for (T* p = elem; p != last; ++p) {
		a.construct(p, val); // 构造元素
	}
}

在这段代码中有两个地方可能引发异常:

  1. 如果内存不足,allocate()​ 可能抛出异常
  2. 如果 T​ 的拷贝构造函数无法拷贝 val​,那么它会抛出异常

改进版:

template<class T, class A>
vector<T, A>::vector(size_type n, const T& val, const A& a)
	: alloc{ a }
{
	elem = alloc.allocate(n); // 分配空间
	std::iterator p;
	try {
		std::iterator end = elem + n;
		for (p = elem; p != end; ++p) {
			alloc.construct(p, val); // 构造元素
		}
		last = space = p;
	}
	catch (...) {
		for (std::iterator q = elem; q != p; ++q) {
			alloc.destroy(q); // 销毁元素
		}
		alloc.deallocate(elem, n); // 释放空间
		throw; // 重新抛出异常
	}
}

在改进版中,如果构造元素失败,则销毁之前成功构造的元素,并释放分配的空间。

此外,还可以使用标准库提供的 uninitialized_fill​,其实现类似如下:

template<class For, class T>
void uninitialized_fill(For beg, For end, const T& x)
{
    For p;
    try
    {
        for(p = beg; p != end; ++p)
        {
            ::new(static_cast<void*>(&*p))T(x);
        }
    }
    catch(...)
    {
        for(For q = beg; q != p; ++q)
            (&*q)->~T();
        throw;
    }
}

其中,构造 &*p​ 主要是为了兼顾非指针的迭代器。

使用 uninitialized_fill ​后可简化如下:

template<class T, class A>
vector<T, A>::vector(size_type n, const T& val, const A& a)
	: alloc{ a }
{
	elem = alloc.allocate(n); // 分配空间
	try {
		std::uninitialized_fill(elem, elem + n, val); // 初始化元素
		last = space = elem + n;
	}
	catch (...) {
		alloc.deallocate(elem, n); // 释放空间
		throw; // 重新抛出异常
	}
}

显式的表示内存

表示 vector 内存的辅助类:

template <class T,class A = std::allocator<T>>
struct vector_base {
	A alloc;
	T* elem;
	T* space;
	T* last;

	vector_base(const A& a, typename A::size_type n)
		: alloc{ a }, elem{ alloc.allocate(n) }, space{ elem + n }, last{ elem + n }
	{};

	~vector_base() {
		alloc.deallocate(elem, last - elem);
	}

	vector_base(const vector_base&) = delete;
	vector_base& operator=(const vector_base&) = delete;

	vector_base(vector_base&&);
	vector_base& operator=(vector_base&&);
};

template<class T,class A>
vector_base<T, A>::vector_base(vector_base&& a)
	: alloc{ a.alloc }, elem{ a.elem }, space{ a.space }, last{ a.last }
{
	a.elem = a.space = a.last = nullptr;
}

template<class T,class A>
vector_base<T, A>& vector_base<T, A>::operator=(vector_base&& a)
{
	std::swap(*this, a);
	return *this;
}

只要 elem ​和 last ​是正确的,vector_base ​就能被销毁。vector_base ​处理的不是类型 T ​的对象,而是类型 T ​的内存。因此,vector_base ​的用户必须在已分配的空间上显式地构造全部对象,并且在 vector_base ​被销毁之前销毁掉 vector_base ​的所有对象。

vector ​中使用 vector_base​:

template<class T, class A = std::allocator<T>>
class vector {
private:
	vector_base<T, A> vb;
	void destroy_elements();
public:
	using size_type = unsigned int; // 表示 vector 尺寸的数据类型

	explicit vector(size_type n, const T& val = T(), const A & = A());

	vector(const vector& a); // 拷贝构造函数
	vector& operator=(const vector& a); // 拷贝赋值运算符

	vector(vector&& a); // 移动构造函数
	vector& operator=(vector&& a); // 移动赋值运算符

	~vector() { destroy_elements(); }; // 析构函数

	size_type size() const { return vb.space - vb.elem; } // 返回 vector 的尺寸
	size_type capacity() const { return vb.last - vb.elem; } // 返回 vector 的容量
	void reserve(size_type n); // 增加空间到 n

	void resize(size_type n, T = {}); // 改变 vector 的尺寸
	void clear() { resize(0); }; // 清空 vector

	void push_back(const T&);
};

template<class T, class A>
vector<T, A>::vector(size_type n, const T& val, const A& a)
	: vb{a,n}
{
	std::uninitialized_fill(vb.elem, vb.elem + n, val);
}
template<class T, class A>
void vector<T, A>::destroy_elements() {
	for (T* p = vb.elem; p != vb.space; ++p) {
		p->~T(); // 调用析构函数
	}
	vb.space = vb.elem;
}

template<class T, class A>
vector<T, A>::vector(const vector<T, A>& a) 
	:vb{ a.alloc,a.size()}
{
	std::uninitialized_copy(a.vb.elem, a.vb.space, vb.elem);
}

template<class T,class A>
vector<T, A>::vector(vector<T, A>&& a) {
	clear();
	std::swap(*this,a);
}

赋值

template<class T,class A>
vector<T, A>& vector<T, A>::operator=(const vector<T, A>& a) {
	vector temp{ a };
	std::swap(*this, temp);
	return *this;
}

请注意,我并没有检查自赋值(v=v​)的情况。=的实现机理是先构造一份拷贝,然后交换二者的内容。因此,即使有自赋值发生也不会有什么问题。加上一条检查语句带来的效益要高于使用另外的 vector 赋值所增加的代价。

在上述两种实现中,还可以进行如下优化:

  1. 如果目标 vector 的容量足够存放新的 vector,则我们无须分配新空间。
  2. 元素赋值的效率显然高于先析构一个元素再构造一个新元素。

基于以上两点,可以优化为:

template<class T,class A>
vector<T, A>& vector<T, A>::operator=(const vector<T, A>& a) {
	if (this == &a) return *this; // 优化自赋值

	if (capacity() < a.size()) { // 分配新的 vector 内容
		vector temp{ a };
		std::swap(*this, temp);
		return *this;
	}

	size_type sz = size();
	size_type asz = a.size();

	vb.alloc = a.vb.alloc;

	if (asz <= sz) {
		std::copy(a.vb.elem, a.vb.elem + asz, vb.elem);
		for (T* p = vb.elem + asz; p != vb.space; ++p) {
			p->~T();
		}
	}
	else {
		std::copy(a.vb.elem, a.vb.elem + sz, vb.elem);
		std::uninitialized_copy(a.vb.elem + sz, a.vb.space, vb.space);
	}
	vb.space = vb.elem + asz;
	return *this;
}

copy() ​算法没有提供强异常安全保障。因此,当 T::operator=() ​在 copy() ​的过程中抛出异常时,被赋值的 vector ​不必是赋值内容的一份拷贝,也无须完全保持不变。例如,我们可以令前 5 个元素是赋值内容的拷贝,而剩下的元素保持不变。而且,当 T::operator=() ​抛出异常之后,即使那个正被拷贝的元素既不是它的旧值也不是赋值内容对应的值,也属于正常情况。换句话说,如果 T::operator=() ​抛出异常时它的运算对象都处于有效状态,则即使 vector ​的状态不是预期中的样子,它也仍然是有效的。

函数 用途 要求
std::copy 将已初始化数据从一个范围复制到另一个范围 目标区域需已初始化
std::uninitialized_copy 将数据从一个范围复制到未初始化区域 目标区域未初始化
std::uninitialized_fill 在未初始化区域用特定值填充数据 目标区域未初始化
std::swap 交换两个对象的值 a ​和 b ​应为相同类型

改变尺寸

reserve()

template<class T, class A>
void vector<T, A>::reserve(size_type n) {
	if (n <= capacity()) return; // 无需分配新的内存

	vector_base<T, A> b{ vb.alloc,n };
	std::uninitialized_move(vb.elem, vb.space, b.elem);
	b.space = b.elem + size();
	std::swap(vb, b);
}

resize()

template<class T,class A>
void vector<T, A>::resize(size_type new_size,T val) {
	reserve(new_size);
	if (size() < new_size) {
		std::uninitialized_fill(elem + size(), elem + new_size, val);
	}
	else {
		for (T* p = vb.elem + new_size; p != vb.space; ++p) {
			p->~T();
		}
	}
	vb.space = vb.elem + new_size;
}

push_back()

template<class T, class A>
void vector<T, A>::push_back(const T& val) {
	if (vb.space == vb.last) {
		reserve(size() == 0 ? 8 : 2 * size());
	}
	vb.alloc.construct(vb.space, val);
	++vb.space;
}

最后一点思考

在 vector 的实现中,没有用到 try 块(除了隐藏在 uninitialized_copy ​中的那个之外)。

我们非常小心的设计操作顺序以确保当抛出异常时,vector 不被改变或者至少处于有效的状态。

与使用 try 块显式地处理错误相比,通过控制操作顺序以及 RAⅡI 技术(见 13.3 节)实现异常安全的方法要更简单有效。但是一旦程序员组织代码的方式有误,那么与缺少异常处理代码相比,前者引发异常安全问题的可能性会大得多。

关于代码顺序最基本的规则是先构建好替代者并确保可以对其正常赋值,再销毁现有的信息。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇