C++白皮书学习

decltype

C++ decltype用法详解-CSDN博客     <-参考文章

 用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来很不方便,甚至压根无法使用。

auto varName=value;
decltype(exp) varName=value;

  •  auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系
  • auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导
  • 而decltype不要求,因此可以写成如下形式

decltype(exp) varName;

原则上将,exp只是一个普通的表达式,它可以是任意复杂的形式,但必须保证exp的结果是有类型的,不能是void;如exp为一个返回值为void的函数时,exp的结果也是void类型,此时会导致编译错误 

decltype 的几种形式

int x = 0;
decltype(x) y = 1;           // y -> int
decltype(x + y) z = 0;       // z -> int
const int& i = x;
decltype(i) j = y;           // j -> const int &
const decltype(z) * p = &z;  // *p  -> const int, p  -> const int *
decltype(z) * pi = &z;       // *pi -> int      , pi -> int *
decltype(pi)* pp = π      // *pp -> int *    , pp -> int * *

推到规则

  • 如果exp是一个不被括号()包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,decltype(exp)的类型和exp一致
  • 如果exp是函数调用,则decltype(exp)的类型就和函数返回值的类型一致
  • 如果exp是一个左值,或被括号()包围,decltype(exp)的类型就是exp的引用,假设exp的类型为T,则decltype(exp)的类型为T&
     

exp中调用函数时需要带上括号和参数,但这仅仅是形式,并不会真的去执行函数代码

class A{
public:
   int x;
}
 
int main()
{
const A obj;
decltype(obj.x) a=0;//a的类型为int
decltype((obj.x)) b=a;//b的类型为int&
 
int n=0,m=0;
decltype(m+n) c=0;//n+m得到一个右值,c的类型为int
decltype(n=n+m) d=c;//n=n+m得到一个左值,d的类型为int &
return 0;
}

左值:表达式执行结束后依然存在的数据,即持久性数据;右值:是指那些在表达式执行结束不再存在的数据,即临时性数据。一个区分的简单方法是:对表达式取地址,如果编译器不报错就是左值,否则为右值

移动语义

RVO(Return  Value Optimization)是一种编译器优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象返回,那么这个临时对象会消耗一个构造函数(Constructor)、一个拷贝构造函数(Copy Constructor)以及一个析构函数(Destructor)的调用的代价,RVO的目的就是消除为保存返回值而创建的临时对象,这样就可以将成本降低到一个构造函数的代价

class Matrix {
    double* elements;    // 指向所有元素的指针
    // ...
public:
    Matrix (Matrix&& a)  // 移动构造
    {
        elements = a.elements;  // 复制句柄
        a.elements = nullptr;   // 现在 a 的析构函数不用做任何事情了
    }
    // ...
};

当用于初始化或赋值的源对象马上就会被销毁时,移动就比拷贝要更好:移动操作只是简单地把对象的内部表示“窃取”过来。&& 表示构造函数是一个移动构造函数Matrix&& 被称为右值引用。当用于模板参数时,右值引用的写法 && 被叫做转发引用

Matrix mx = m1+m2+m3;  // 不需要临时变量
string sx = s1+s2+s3;  // 不需要临时变量

右值引用可以用于给现有类方便地添加移动语义。意思是说,拷贝构造函数和赋值运算符可以根据实参是左值还是右值来进行重载。当实参是右值时,类的作者就知道他拥有对该实参的唯一引用。

一个突出的例子是生成“智能指针”的工厂函数:

template 
std::shared_ptr factory(A1&& a1)
{
    return std::shared_ptr(new T(std::forward(a1)));
}
template 
class clone_ptr
{
private:
    T* ptr;
public:
    // ...
    clone_ptr(clone_ptr&& p)            // 移动构造函数
        : ptr(p.ptr)    // 拷贝数据的表示
    {
        p.ptr = 0;      // 把源数据的表示置空
    }
    clone_ptr& operator=(clone_ptr&& p) // 移动赋值
    {
        std::swap(ptr, p.ptr);
        return *this;   // 销毁目标的旧值
    }
};

资源管理指针

void newer_use(Args a)
{
    auto p = unique_ptr(new Blob(a));
    // ...
    if (foo) throw Bad();  // 不会泄漏
    if (bar) return;       // 不会泄漏
    // ...
}

这种写法更简短、更安全,迅速就流行开去。不过,“智能指针”仍然被过度使用:“它们的确智能,但它们仍然是指针。”除非我们确实需要指针,否则,简单地使用局部变量会更好:

void simplest_use(Args a)
{
    Blob b(a);
    // ...
    if (foo) throw Bad(); // 不会泄漏
    if (bar) return;      // 不会泄漏
    // ...
}

nullptr

在 C 和 C++ 中,如果将字面量 0 赋值给指针或与指针比较时它表示空指针。更令人困惑的是,如果将任何求值为零的整数常量表达式赋值给指针或与指针比较时它也表示空指针。例如:

int* p = 99-55-44; // 空指针
int* q = 2;        // 错误:2 是一个 int,而不是一个指针

标准库宏 NULL(从 C 中采用),它在标准 C++ 中定义为 0。某些编译器会对 int* p = 0 提出警告

int* p0 = nullptr;
int* p1 = 99-55-44;  // 可以,为了兼容性
int* p2 = NULL;      // 可以,为了兼容性

int f(char*);
int f(int);

int x1 = f(nullptr); // f(char*)
int x2 = f(0);       // f(int)

constexpr 函数

constexpr 函数可以在编译期进行求值,因此它无法访问非本地对象(它们在编译时还不存在),因此 C++ 获得了一种纯函数。

为什么我们要求程序员应该使用 constexpr 来标记可以在编译期执行的函数?原则上,编译器可以弄清楚在编译期可以计算出什么,但是如果没有标注,用户将受制于各种编译器的聪明程度,并且编译器需要将所有函数体“永远”保留下来,以备常量表达式在求值时要用到它们。我们选择 constexpr 一词是因为它足够好记,但又“足够奇怪”而不会破坏现有代码。

在某些地方,C++ 需要常量表达式(例如,数组边界和 case 标签)。另外,我们可以通过将变量声明为 constexpr 来要求它在编译期被初始化:

constexpr LengthInKM marks[] = { LengthInMile(2.3), LengthInMile(0.76) };

void f(int x)
{
    int y1 = x;
    constexpr int y2 = x;   // 错误:x 不是一个常量
    constexpr int y3 = 77;  // 正确
}

 C++之constexpr详解-CSDN博客

必须明确一点,在constexpr声明中如果定义了一个指针,限定符conxtexpr仅对指针有效,与指针所指的对象无关。

const int*p = nullptr;        //p是一个指向整形常量的指针
constexpr int* q = nullptr;   //q是一个指向整数的常量指针

属性

在程序中,属性提供了一种将本质上任意的信息与程序中的实体相关联的方法。例如:

[[noreturn]] void forever()
{
    for (;;) {
        do_work();
        wait(10s);
    }
}

属性 [[noreturn]] 通知编译器或其他工具 forever() 永远不会返回,这样它就可以抑制关于缺少返回的警告。属性用 [[…]] 括起来。

lambda

然而,我们可以指定 lambda 表达式应该从它的环境中“捕获”一些或所有的变量。回调是 lambda 表达式的一个常见用例,因为操作通常只需要写一次,并且操作会需要安装该回调的代码上下文中的一些信息。

void test()
{
    string s;
    // ... 为 s 计算一个合适的值 ...
    w.foo_callback([&s](int i){ do_foo(i,s); });
    w.bar_callback([=s](double d){ return do_bar(d,s); });
}

[&s] 表示 do_foo(i,s) 可以使用 ss 通过引用来传递(“捕获”)。[=s] 表示 do_bar(d,s) 可以使用 ss 是通过值传递的。如果回调函数在与 test 相同的线程上被调用,[&s] 捕获可能效率更高,因为 s 没有被复制。如果回调函数在不同的线程上被调用,[&s] 捕获可能是一个灾难,因为 s 在被使用之前可能会超出作用域;这种情况下,我们想要一份副本。一个 [=] 捕获列表意味着“将所有局部变量复制到 lambda 表达式中”。而一个 [&] 捕获列表意味着“lambda 表达式可以通过引用指代所有局部变量”,并意味着 lambda 表达式可以简单地实现为一个局部函数。

别名

template class X { /* ... */ };
template typedef X Xi;  // 定义别名
Xi Ddi;                            // 相当于 X

typedef double (*analysis_fp)(const vector&);

using analysis_fp = double (*)(const vector&);

tuple

它主要用来返回成对的值,比如两个迭代器或者一个指针加上一个成功标志。 

元组是大小固定而成员类型可以不同的容器。作为一种通用的辅助工具,它们增加了语言的表现力。举几个元组类型一般用法的例子:

  • 作为返回类型,用于需要超过一个返回类型的函数
  • 编组相关的类型或对象(如参数列表中的各条目)成为单个条目
  • 同时赋多个值
auto SVD(const Matrix& A) -> tuple
{
    Matrix U, V;
    Vector S;
    // ...
    return make_tuple(U,S,V);
};

void use()
{
    Matrix A, U, V;
    Vector S;
    // ...
    tie(U,S,V) = SVD(A); // 使用元组形式
}

在这里,make_tuple() 是标准库函数,可以从参数中推导元素类型来构造 tupletie() 是标准库函数,可以把 tuple 的成员赋给有名字的变量。

静态类型

依赖静态类型安全有两大好处:

  • 明确意图
    • 帮助程序员直接表达想法
    • 帮助编译器捕获更多错误
  • 帮助编译器生成更好的代码。

noexcept规约

void do_something(int n) noexcept
{
    vector v(n);
    // ...
}

如果 do_something() 抛异常,程序会被终止。这样操作恰好非常接近零开销,因为它简单地短路了通常的异常传播机制

还有一个条件版本的 noexcept,用它可以写出这样的模板,其实现依赖于某参数是否会抛异常。这是最初促成 noexcept 的用例。例如,下面代码中,当且仅当 pair 的两个元素都有不抛异常的移动构造函数时,pair 的移动构造函数才会声明不抛异常:

template
class pair {
    // ...
    template 
    pair(pair&& rhs)
        noexcept(is_nothrow_constructible::value
              && is_nothrow_constructible::value)
    : first(move(rhs.first)),
      second(move(rhs.second))
    {}
    // ...
};

数字分隔符

auto a = 1'234'567;    // 1234567(整数)
auto b = 1'234'567s;   // 1234567 秒

概念

对 C++ 来说,泛型编程和使用模板的元编程已经取得了巨大的成功。但是,对泛型组件的接口却迟迟未能以一种令人满意的方式进行合适的规范。例如,在 C++98 中,标准库算法大致是如下规定的:

template
ForwardIterator find(Forward_iterator first, Forward_iterator last,
                              const Value & val)
{
    while (first != last && *first != val)
        ++first;
    return first;
}

C++ 标准规定:

  • 第一个模板参数必须是前向迭代器。
  • 第二个模板参数类型必须能够使用 == 与该迭代器的值类型进行比较。
  • 前两个函数参数必须标示出一个序列。

这些要求是隐含在代码中的:编译器所要做的就是在函数体中使用模板参数。结果是:极大的灵活性,对正确调用生成出色的代码,以及对不正确的调用有糟糕得一塌糊涂的错误信息。解决方案显而易见,将前两项条件作为模板接口的一部分来指定:

template
    requires equality_comparable
forward_iterator find(Iter first, Iter last, const Value& val);

C++20四大之三:concept特性详解_c++20 concept-CSDN博客

C++ 17

  • optional——持有 T 或什么都不持有
  • variant——持有 T 或 U
  • any——持有任意类型
optional var1 = 7;
variant var2 = 7;
any var3 = 7;

auto x1 = *var1 ;               // 对 optional 解引用
auto x2 = get(var2);       // 像访问 tuple 一样访问 variant
auto x3 = any_cast(var3);  // 转换 any

为了提取存储的值,需要使用三种不兼容的写法之一。这对程序员来讲是一种负担。没错,有经验的程序员会习惯的,但这种非要人们去习惯的不规则性本就不该存在。

为了简化 variant 的使用,有一种访问者机制。首先我们需要一个辅助模板去定义一个重载集合:

// 简单访问的样板:
template struct overloaded : Ts... { using Ts::operator()...; };
template overloaded(Ts...) -> overloaded;

并发

在 C++17 中,以下类型的加入极大地简化了锁的使用:

  • scoped_lock——获取任意数量的锁,而不会造成死锁
  • shared_mutex 和 shared_lock——实现读写锁

例如,我们能获取多个锁,而不用担心会产生死锁:

void f()
{
    scoped_lock lck {mutex1, mutex2, mutex3}; // 获得所有三把锁
    // ... 操作共享数据 ...
} // 隐式地释放所有锁
shared_mutex mx;    // 一个可以被共享的锁
void reader()
{
    shared_lock lck {mx};  // 跟其他 reader 共享访问
    // ... 读 ...
}
void writer()
{
    unique_lock lck {mx};  // writer 需要独占访问
    // ... 写 ...
}

多个读线程可以“共享”该锁(即同时进入临界区),而写线程则需要独占访问。

未完待续......

你可能感兴趣的:(C++,c++)