Skip to content

Latest commit

 

History

History
440 lines (376 loc) · 15.4 KB

ch28.md

File metadata and controls

440 lines (376 loc) · 15.4 KB

Chapter28 标准库的其他微小特性和修改

C++标准库还有一些微小的扩展和变化,将在这一章中描述。

28.1 std::uncaught_exceptions()

C++的一个关键模式是 RAII: Resource Acquisition Is Initialization 。 这是一种安全地处理那些你必须要释放或清理的资源的方式。 在构造一个对象时把需要的资源的所有权传递给它,当离开作用域时它的析构函数就会自动释放资源。 这样做的好处是即使因为异常而离开当前作用域时也能保证释放资源。

然而,有时资源的“释放操作”依赖于我们到底是正常执行离开了作用域还是因为异常而意外离开了作用域。 一个例子是事务性的资源,如果我们正常执行离开作用域我们可能想进行 提交 操作, 而当因为异常离开作用域时想进行 回滚 操作。

为了达到这个目的,C++11引入了std::uncaught_exception(),其用法如下所示:

class Request {
public:
    ...
    ~Request() {
        if (std::uncaught_exception()) {
            rollback();
        }
        else {
            commit();
        }
    }
};

因此,当Request对象离开作用域时会根据是否有异常抛出来 决定到底是调用commit()还是rollback()

{
    Request r1{...};    // 没有异常时析构函数会调用commit()
    ...
    if (...) {
        throw ...;      // 让析构函数调用rollback()
    }
    ...
}   // 正常时调用commit(),异常时调用rollback()

然而,在如下使用场景中这个API不能正常工作: 当我们正在处理异常时 如果创建了新的Request对象,那么即使在使用它的期间没有异常抛出 它的析构函数也总是会调用rollback()

try {
    ...
}
catch (...) {
    Request r2{...};
    ...
}   // 即使在catch语句块中没有出现新的异常也会调用rollback()

新的标准库函数uncaught_exceptions()(注意名字最后多出来的s) 解决了这个问题。它返回有多少个(嵌套的)还未处理的异常,而不是我们是否正在处理一个异常。 这允许我们查明是否有额外的异常抛出(即使在处理异常时)。

有了这个,我们可以简单的对Request的定义做如下修改:

class Request {
private:
    int initialUncaught{std::uncaught_exceptions()};
public:
    ...
    ~Request() {
        if (std::uncaught_exceptions() > initialUncaught) {
            rollback();
        }
        else {
            commit();
        }
    }
};

现在下面两个示例场景都能正常工作:

try {
    Request r1{...};    // 没有异常时析构函数会调用commit()
    ...
    if (...) {
        throw ...;      // 让析构函数调用rollback()
    }
    ...
}   // 正常时调用commit(),异常时调用rollback()
catch (...) {
    Request r2{...};
    ...
}   // 如果没有额外的异常发生就调用commit()

这里,r2的构造函数把initialUncaught初始化为1, 因为我们已经在catch语句块中处理了一个异常。 然而,当r2的析构函数调用时没有新的异常抛出,所以initialUncaught仍然 是1,因此会调用commit()。如果在catch子句中又抛出了第二个未 捕获的异常那么std::uncaught_exceptions()将会返回2,因此r2的 析构函数会调用rollback()

lib/uncaught.cpp 获取完整的示例。

旧的API std::uncaught_exception()(没有末尾的s)自从C++17起被废弃,不应该再被使用。

28.2 共享指针改进

C++17中也添加了一些共享指针的改进。

另外,注意成员函数unique()已经被废弃了。

28.2.1 对原生C数组的共享指针的特殊处理

自从C++17起,你可以显式的声明数组的共享指针来确保deleter会调用delete[] (自从C++11起独占指针就已经可以做到):你现在可以简单地调用:

std::shared_ptr<std::string[]> p{new std::string[10]};

来代替:

std::shared_ptr<std::string> p{new std::string[10], std::default_delete<std::string[]>()};

或者:

std::shared_ptr<std::string> p{new std::string[10], [](std::string* p) {
                                                        delete[] p;
                                                    }};

当实例化的是数组时,API也会有一些变化:不再是使用operator*,而是使 用operator[](就像独占指针一样):

std::shared_ptr<std::string> ps{new std::string};
*ps = "hello";      // OK
ps[0] = "hello";    // ERROR

std::shared_ptr<std::string[]> parr{new std::string[10]};
*parr = "hello";    // ERROR(未定义行为)
parr[0] = "hello";  // OK

注意分配的是原生数组时是否支持operator*和分配的不是数组时是否支持operator[] 还没有明确的定义。然而,通常情况下调用这些未定义的运算符不能编译。

28.2.2 共享指针的reinterpret_pointer_cast

除了static_pointer_castdynamic_pointer_castconst_pointer_cast之外, 你现在还可以调用reinterpret_pointer_cast重新解释一个共享指针指向的若干位的类型。

28.2.3 共享指针的weak_type

为了支持在泛型代码中使用弱指针,共享指针类现在提供了一个新的成员weak_type。例如:

template<typename T>
void observe(T sp)
{
    // 用传入的共享指针初始化弱指针
    typename T::weak_type wp{sp};
    ...
}

28.2.4 共享指针的weak_from_this

有时你需要另一个智能指针来指向一个已经存在的对象,并且不想访问已经指向该对象的其他共享指针。 为了解决这个问题,C++11引入了基类enable_shared_from_this, 它提供了成员函数shared_from_this

#include <memory>

class Person : public std::enable_shared_from_this<Person>
{
    ...
};

Person* pp = new Person{...};
std::shared_ptr<Person> sp1{pp};    // sp1获得了所有权
std::shared_ptr<Person> sp2{pp};    // 运行时错误:sp2不能再获取所有权
std::shared_ptr<Person> sp3{pp->shared_from_this()};    // OK:sp1和sp3共享所有权

如果该对象的一个成员函数需要返回一个自身的共享指针时就会用到这个特性:

class Person : public std::enable_shared_from_this<Person>
{
    ...
    std::shared_ptr<Person> sharedPtrTo() {
        return shared_from_this();
    }
}

自从C++17开始,有一个额外的辅助函数可以返回一个指向对象的弱指针:

Person* pp = new Person{...};
std::shared_ptr<Person> sp{pp};                 // sp获得了所有权
...
std::weak_ptr<Person> wp{pp->weak_from_this()}; // wp分享了sp拥有的所有权

在C++17之前,你可以用如下代码实现同样的功能:

weak_ptr<Person> wp{pp->shared_from_this()}

但是weak_from_this()可以在修改更少的引用计数的情况下实现相同的效果。 另外,还要说明一种特殊的边界情况:如果一个原生指针被传递给两个不同的共享指针 (如果它们中只有一个最终释放了资源那么这是有效的),那么shared_from_this()weak_from_this()会分享第一个共享指针的所有权。 这意味着下面的代码是可行的:

struct Person : public std::enable_shared_from_this<Person>
{
    ...
};

Person* p = new Person;
std::shared_ptr<Person> sp1(p);         // 创建第一个共享指针

{
    std::shared_ptr<Person> sp2(p,      // 创建第二个不会释放资源的共享指针
                                [] (void*) {
                                });
    auto sp3{p->shared_from_this()};    // sp3分享sp1的所有权
}

auto sp4{p->shared_from_this()};        // sp4分享sp1的所有权

在这个情况得到说明之前,一些实现让sp3sp4分享 最后 创建的 共享指针的所有权,这会导致当初始化时sp4抛出std::bad_weak_ptr异常。

28.3 数学扩展

C++17还引入了下面的数学函数。

28.3.1 最大公约数和最小公倍数

在头文件<numeric>中:

  • gcd(x, y)返回xy最大公约数
  • lcm(x, y)返回xy最小公倍数

参数的类型都是除了bool以外的整数类型。两个参数的类型可以不同, 此时返回值的类型将是两个参数的公共类型。

例如:

#include <numeric>

int i{42};
long l{30};

auto x{std::gcd(i, l)};     // x是long 6
auto y{std::lcm(i, l)};     // y是long 210

28.3.2 std::hypot()的三参数重载

在头文件中<cmath>中:

  • hypot(x, y, z)返回三个参数的平方之和的平方根。

就像<cmath>中其他的函数一样,这个函数有支持所有浮点数类型的重载版本。

这些重载在math.h或者命名空间std之外是没有的。

例如:

#include <cmath>

// 计算3D坐标下两个点的距离:
auto dist = std::hypot(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z);

在C++17之前,你必须像下面这样调用:

auto dist = std::hypot(p2.x - p1.x, std::hypot(p2.y - p1.y, p2.z - p1.z));

28.3.3 数学的特殊函数

表数学的特殊函数中的一部分已经在国际标准IS 29124:2010中标准化, 现在被要求无条件的加入C++标准中的头文件<cmath>中。

名称 含义
assoc_laguerre() 关联Laguerre多项式
assoc_legendre() 关联Legendre函数
beta() beta函数
comp_ellint_1() 第一类完整椭圆积分
comp_ellint_2() 第二类完整椭圆积分
comp_ellint_3() 第三类完整椭圆积分
cyl_bessel_i() 规则圆柱贝塞尔函数变体
cyl_bessel_j() 第一类圆柱贝塞尔函数
cyl_bessel_k() 不规则圆柱贝塞尔函数变体
cyl_neumann() 圆柱诺依曼函数(第二类圆柱贝塞尔函数)
ellint_1() 第一类不完整椭圆积分
ellint_2() 第二类不完整椭圆积分
ellint_3() 第三类不完整椭圆积分
expint() 指数积分
hermite() Hermite多项式
laguerre() Laguerre多项式
legendre() Legendre多项式
riemann_zeta() 黎曼zeta函数
sph_bessel() 第一类球形贝塞尔函数
sph_legendre() 关联球形Legendre函数
sph_neumann() 球形诺依曼函数(第二类球形贝塞尔函数)

这些函数的参数都是浮点数,返回值是double。 所有这些函数还有

  • 后缀f代表参数和返回值的类型是float
  • 后缀l代表参数和返回值的类型是long double

例如:

    // 指数积分:
    double      expint(double x);
    float       expintf(float x);
    long double expintl(long double x);

    // Laguerre多项式:
    double      laguerre(unsigned n, double x);
    float       laguerref(unsigned n, float x);
    long double laguerrel(unsigned n, long double x);

28.4 chrono扩展

C++17还向标准库中的时间库部分添加了一些扩展来增强库的易用性和一致性。

对于时间段和时间点,还添加了新的舍入函数:

  • round():舍入到最近的整数值
  • floor():向负无穷舍入到最近的整数值
  • ceil():向正无穷舍入到最近的整数值

这些舍入方法和duration_cast<>time_point_cast<> (在C++17之前就已经存在)不同,新的舍入函数不是简单的向0截断。

另外,时间段类型还添加了缺失的abs()函数。

下面的程序演示了时间段类型的行为:

#include <chrono>
#include <iostream>

std::ostream &operator<<(std::ostream &strm,
                         const std::chrono::duration<double, std::milli>& dur)
{
    return strm << dur.count() << "ms";
}

template<typename T>
void roundAndAbs(T dur)
{
    using namespace std::chrono;

    std::cout << dur << '\n';
    std::cout << " abs():   " << abs(dur) << '\n';
    std::cout << " cast:    " << duration_cast<std::chrono::seconds>(dur) << '\n';
    std::cout << " floor(): " << floor<std::chrono::seconds>(dur) << '\n';
    std::cout << " ceil():  " << ceil<std::chrono::seconds>(dur) << '\n';
    std::cout << " round(): " << round<std::chrono::seconds>(dur) << '\n';
}

int main()
{
    using namespace std::literals;
    roundAndAbs(3.33s);
    roundAndAbs(3.77s);
    roundAndAbs(-3.77s);
}

它会有如下输出:

3330ms
abs():   3330ms
cast:    3000ms
floor(): 3000ms
ceil():  4000ms
roumd(): 3000ms
3770ms
abs():   3770ms
cast:    3000ms
floor(): 3000ms
ceil():  4000ms
round(): 4000ms
-3770ms
abs():   3770ms
cast:    -3000ms
floor(): -4000ms
ceil():  -3000ms
round(): -4000ms

28.5 constexpr扩展和修正

就像从C++11开始的每个新版本一样,标准中又在很多地方添加/修正了对constexpr的支持。

最重要的修正有:

  • 对于std::array,下面的函数现在是constexpr

    • begin()、end()、cbegin()、cend()、rbegin()、rend()、crbegin()、crend()
    • 非常量数组的operator[]、at()、front()、back()
    • data()
  • 范围访问的泛型独立函数(std::begin()、std::end()、std::rbegin()、std::rend()) 和辅助函数(std::advance()、std::distance()、std:prev()、std::next())现在也是constexpr

  • std::reverse_iteratorstd::move_iterator的所有操作现在都是constexpr

  • C++标准库里整个时间库部分(time_pointduration、时钟、ratio), 现在除了时钟的成员函数now()to_time_t()from_time_t()之外 所有操作和变量都是constexpr

  • 所有的std::char_traits特化的成员函数是constexpr。特别的, 这允许我们在编译期初始化字符串视图。

例如,你现在可以写:

constexpr std::array arr{0, 8, 15, 42};
constexpr auto val = arr[2];    // OK
static_assert(val == 15);       // OK,不会断言失败

注意对于迭代器,我们需要使用全局的或者static的对象,因为我们不能在编译期 获取栈上的对象的地址:

constexpr static std::array arr{0, 8, 15, 42};
constexpr auto pos = std::next(arr.begin());    // OK
static_assert(*pos == 15);                      // OK,不会断言失败

28.6 noexcept扩展和修正

就像从C++11开始的每个新版本一样,标准中又在很多地方添加/修正了对noexcept的支持。

最重要的修正有:

  • 对于std::vector<>std::string(std::basic_string<>), C++17保证下列操作不会抛出异常

    • 默认构造函数(前提是提供的分配器的默认构造函数不会抛出异常)
    • 移动构造函数
    • 以分配器为参数的构造函数
  • 对于所有的容器(包括std::string/std::basic_string<>), C++17保证下列操作不会抛出异常

    • 移动赋值运算符(前提是提供的分配器可以互换)
    • swap()函数(前提是提供的分配器可以互换)

对vector重新分配的影响

注意noexcept的修正中有一项非常特殊也非常重要: 只有vector和string现在保证不会在移动构造函数中抛出异常。 其他的容器仍然有可能抛出异常。

这一点当在一个vector中使用这几种类型时会产生重要的影响, 如果元素的移动构造函数保证不抛出异常, 那么vector在重新分配时可以简单的使用移动构造函数来移动元素。

换句话说:

  • 重新分配一个string/vector的vector现在保证会很快速。
  • 重新分配一个其他容器类型的vector仍然可能很慢。

这已经成为了另一个除非必要否则使用vector作为默认容器的原因。