《深入理解C++11》笔记(第四章. 新兵易学,老兵易用)

今天二刷《深入理解C++11》,就顺带把我在印象笔记的摘录传到CSND上,禁止转载!!!

全部笔记链接:

新手易学,老兵易用

1 右尖括号>的改进

2 auto类型推导

2.1 静态类型、动态类型与类型推导
  • 从技术上严格地讲,静态类型和动态类型的主要区别在于对变量进行类型检查的时间点。对于所谓的静态类型,类型检查主要发生在编译阶段;而对于动态类型,类型检查主要发生在运行阶段。形如Python等语言中变量“拿来就用”的特性,则需要归功于一个技术,即类型推导。
  • auto声明变量的类型必须由编译器在编译时期推导而得。
  • auto声明的变量必须被初始化,以使编译器能够从其初始化表达式中推导出其类型。从这个意义上来讲,auto并非一种“类型”声明,而是一个类型声明时的“占位符”,编译器在编译时期会将auto替代为变量实际的类型。
2.2 auto优势
  • 最大优势就是在拥有初始化表达式的复杂类型变量声明时简化代码。
  • 第二个优势则在于可以免除程序员在一些类型声明时的麻烦,或者避免一些在类型声明时的错误。
  • 第三个优点就是其“自适应”性能够在一定程度上支持泛型的编程。
#define Max1(a, b) ((a) > (b)) ? (a) : (b)
#define Max2(a, b) ({ \
        auto _a = (a); \
        auto _b = (b); \
        (_a > _b) ? _a: _b; })
int main() {
    int m1 = Max1(1*2*3*4, 5+6+7+8);
    int m2 = Max2(1*2*3*4, 5+6+7+8);
}
// 编译选项:g++ -std=c++11 4-2-8.cpp

性能Max1 < Max2,因为 Max2少计算一次a或者b的值
这个与我不愿意写复杂的三元运算符表达式的愿意惊人的一致

2.3 auto的使用细则
  • 如果要使得auto声明的变量是另一个变量的引用,则必须使用auto &,而对于指针,auto 与 auto*一样。
  • C++11标准规定auto可以与cv限制符一起使用,不过声明为auto的变量并不能从其初始化表达式中“带走”cv限制符(const && volatile)。
double foo();
float * bar();
const auto a = foo();         // a: const double
const auto & b = foo();      // b: const double&
volatile auto * c = bar();   // c: volatile float*
auto d = a;                      // d: double
auto & e = a;                   // e: const double &
auto f = c;                      // f: float *
volatile auto & g = c;        // g: volatile float * &
// 编译选项:g++ -std=c++11 4-2-10.cpp

这里的例外还是引用,可以看出,声明为引用的变量e,g都保持了其引用的对象的相同属性(事实上,对于指针也是一样的)。

  • 用auto来声明多个变量类型时,只有第一个变量用于auto的类型推导,然后推导出来的数据类型被作用于其他的变量
  • auto的使用限制
#include <vector>
using namespace std;
void fun(auto x =1){}  // 1: auto函数参数,无法通过编译
struct str{
    auto var = 10;    // 2: auto非静态成员变量,无法通过编译
};
int main() {
    char x[3];
    auto y = x;
    auto z[3] = x; // 3: auto数组,无法通过编译
    // 4: auto模板参数(实例化时),无法通过编译
    vector<auto> v = {1};
}
// 编译选项:g++ -std=c++11 4-2-13.cpp

auto是不能做形参的类型的。如果程序员需要泛型的参数,还是需要求助于模板。

对于结构体来说,非静态成员变量的类型不能是auto的。

声明auto数组。

在实例化模板的时候使用auto作为模板参数.

  • 此外,程序员还应该注意,由于为了避免和C++98中auto的含义发生混淆,C++11只保留auto作为类型指示符的用法,以下的语句在C++98和C语言中都是合法的,但在C++11中,编译器则会报错。

      auto int i = 1;
    

3 decltype

3.1 typeid与decltype
  • RTTI(运行时类型识别)

RTTI的机制是为每个类型产生一个type_info类型的数据,程序员可以在程序中使用typeid随时查询一个变量的类型,typeid就会返回变量相应的type_info数据。而type_info的name成员函数可以返回类型的名字。而在C++11中,又增加了hash_code这个成员函数,返回该类型唯一的哈希值,以供程序员对变量的类型随时进行比较。

#include <iostream>
#include <typeinfo>
using namespace std;
class White{};
class Black{};
int main() {
    White a;
    Black b;
    cout << typeid(a).name() << endl;    // 5White
    cout << typeid(b).name() << endl;    // 5Black
    White c;
    bool a_b_sametype = (typeid(a).hash_code() == typeid(b).hash_code());
      bool a_c_sametype = (typeid(a).hash_code() == typeid(c).hash_code());
      cout << "Same type? " << endl;
      cout << "A and B? " << (int)a_b_sametype << endl;    // 0
      cout << "A and C? " << (int)a_c_sametype << endl;    // 1
  }
  // 编译选项:g++ -std=c++11 4-3-1.cpp

is_same模板函数的成员类型value在编译时得到信息,hash_code是运行时得到的信息

  • RTTI会带来一些运行时的开销,所以一些编译器会让用户选择性地关闭该特性(比如XL C/C++编译器的-qnortti,GCC的选项-fno-rttion,或者微软编译器选项/GR-)。且很多时候,运行时才确定出类型对于程序员来说为时过晚,程序员更多需要的是在编译时期确定出类型(标准库中非常常见)。而通常程序员是要使用这样的类型而不是识别该类型,因此RTTI无法满足需求。

  • decltype的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,decltype总是以一个普通的表达式为参数,返回该表达式的类型。而与auto相同的是,作为一个类型指示符,decltype可以将获得的类型来定义另外一个变量。与auto相同,decltype类型推导也是在编译时进行的

#include <typeinfo>
#include <iostream>
using namespace std;
int main() {
    int i;
    decltype(i) j = 0;
    cout << typeid(j).name() << endl;    // 打印出"i", g++表示int
    float a;
    double b;
    decltype(a + b) c;
    cout << typeid(c).name() << endl;    // 打印出"d", g++表示double
}
// 编译选项:g++ -std=c++11 4-3-2.cpp
3.2 decltype的应用
  • decltype与typdef/using的合用
using size_t = decltype(sizeof(0));
using ptrdiff_t = decltype((int*)0- (int*)0);
using nullptr_t = decltype(nullptr);

颠覆了之前类型拓展需要将扩展类型“映射”到基本类型的常规做法。

  • 重用匿名类型
enum class{K1, K2, K3}anon_e;    // 匿名的强类型枚举
union {
    decltype(anon_e) key;
    char* name;
}anon_u;     // 匿名的union联合体
struct {
    int d;
    decltype(anon_u) id;
}anon_s[100];    // 匿名的struct数组
int main() {
    decltype(anon_s) as;
    as[0].id.key = decltype(anon_e)::K1;    // 引用匿名强类型枚举中的值
}
// 编译选项:g++ -std=c++11 4-3-4.cpp

匿名一般都有匿名理由,一般程序员都不希望匿名后的类型被重用。这里的decltype只是提供了一种语法上的可能。

  • 可以适当扩大模板泛型的能力
// s的类型被声明为decltype(t1 + t2)
template <typename T1, typename T2>
void Sum(T1 & t1, T2 & t2, decltype(t1 + t2) & s) {
    s = t1 + t2;
}
int main() {
    int a = 3;
    long b = 5;
    float c = 1.0f, d = 2.3f;
    long e;
    float f;
    Sum(a, b, e);    // s的类型被推导为long
    Sum(c, d, f);    // s的类型被推导为float
}
// 编译选项:g++ -std=c++11 4-3-5.cpp

不过这里还是有一定的限制,我们可以看到返回值的类型必须一开始就被指定,程序员必须清楚Sum运算的结果使用什么样的类型来存储是合适的,这在一些泛型编程中依然不能满足要求。解决的方法是结合decltype与auto关键字,使用追踪返回类型的函数定义来使得编译器对函数返回值进行推导。
事实上,decltype一个最大的用途就是用在追踪返回类型的函数中

  • 注意
  1. 实例1
#include <map>
using namespace std;
int hash(char*);
map<char*, decltype(hash)> dict_key;     // 无法通过编译
map<char*, decltype(hash(nullptr))> dict_key1;
// 编译选项:g++ -c -std=c++11 4-3-7.cpp

decltype只能接受表达式做参数,像函数名做参数的表达式decltype(hash)是无法通过编译的

  1. 实例2
#include <type_traits>
using namespace std;
typedef double (*func)();
int main() {
    result_of<func()>::type f;    // 由func()推导其结果类型
}
// 编译选项:g++ -std=c++11 4-3-8.cpp

f的类型最终被推导为double,而result_of并没有真正调用func()这个函数,一切都是因为底层实现了decltype。

result_of的一个可能的实现方式如下:

template<class>
struct result_of;
template<class F, class... ArgTypes>
struct result_of<F(ArgTypes...)>
{
    typedef decltype(
                      std::declval<F>()(std::declval<ArgTypes>()...)
                      ) type;
};
3.3 decltype推导四规则
  • 具体地,当程序员用decltype(e)来获取类型时,编译器将依序判断以下四规则:

    1. 如果e是一个没有带括号的标记符表达式(id-expression)或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译时错误。
    2. 否则,假设e的类型是T,如果e是一个将亡值(xvalue),那么decltype(e)为T&&。
    3. 否则,假设e的类型是T,如果e是一个左值,则decltype(e)为T&。
    4. 否则,假设e的类型是T,则decltype(e)为T。
  • 标记符表达式(id-expression)
    基本上,所有除去关键字、字面量等编译器需要使用的标记之外的程序员自定义的标记(token)都可以是标记符(identifier)
    单个标记符对应的表达式就是标记符表达式

  • 实例

int i = 4;
int arr[5] = {0};
int *ptr = arr;
struct S { double d; } s;
void Overloaded(int);
void Overloaded(char);       // 重载的函数
int && RvalRef();
const bool Func(int);
// 规则1: 单个标记符表达式以及访问类成员,推导为本类型
decltype(arr) var1;              // int[5], 标记符表达式
decltype(ptr) var2;              // int*, 标记符表达式
decltype(s.d) var4;              // double, 成员访问表达式
decltype(Overloaded) var5;      // 无法通过编译,是个重载的函数
// 规则2: 将亡值,推导为类型的右值引用
decltype(RvalRef()) var6 = 1;   // int&&
// 规则3: 左值,推导为类型的引用
decltype(true ? i : i) var7 = i;     // int&, 三元运算符,这里返回一个i的左值
decltype((i)) var8 = i;                // int&, 带圆括号的左值
decltype(++i) var9 = i;                // int&, ++i返回i的左值
decltype(arr[3]) var10 = i;           // int& []操作返回左值
decltype(*ptr)  var11 = i;            // int& *操作返回左值
decltype("lval") var12 = "lval";     // const char(&)[9], 字符串字面常量为左值
// 规则4:以上都不是,推导为本类型
decltype(1) var13;                 // int, 除字符串外字面常量为右值
decltype(i++) var14;              // int, i++返回右值
decltype((Func(1))) var15;       // const bool, 圆括号可以忽略
// 编译选项:g++ -std=c++11-c 4-3-10.cpp
  • 辅助判断工具

is_lvalue_reference && is_rvalue_reference

#include <type_traits>
#include <iostream>
using namespace std;
int i = 4;
int arr[5] = {0};
int *ptr = arr;
int && RvalRef();
int main(){
    cout << is_rvalue_reference<decltype(RvalRef())>::value << endl;       // 1
    cout << is_lvalue_reference<decltype(true ? i : i)>::value << endl; // 1
    cout << is_lvalue_reference<decltype((i))>::value << endl;             // 1
    cout << is_lvalue_reference<decltype(++i)>::value << endl;             // 1
    cout << is_lvalue_reference<decltype(arr[3])>::value << endl;          // 1
    cout << is_lvalue_reference<decltype(*ptr)>::value << endl;            // 1
    cout << is_lvalue_reference<decltype("lval")>::value << endl;          // 1
    cout << is_lvalue_reference<decltype(i++)>::value << endl;             // 0
    cout << is_rvalue_reference<decltype(i++)>::value << endl;             // 0
}
// 编译选项:g++ -std=c++11 4-3-11.cpp
3.4 cv限制符的继承与冗余的符号
  • 与auto类型推导时不能“带走”cv限制符不同,decltype是能够“带走”表达式的cv限制符的。不过,如果对象的定义中有const或volatile限制符,使用decltype进行推导时,其成员不会继承const或volatile限制符。
#include <type_traits>
#include <iostream>
using namespace std;
const int ic = 0;
volatile int iv;
struct S { int i; };
const S a = {0};
volatile S b;
volatile S* p = &b;
int main() {
    cout << is_const<decltype(ic)>::value << endl;        // 1
    cout << is_volatile<decltype(iv)>::value << endl;    // 1
    cout << is_const<decltype(a)>::value << endl;         // 1
    cout << is_volatile<decltype(b)>::value << endl;     // 1
    cout << is_const<decltype(a.i)>::value << endl;      // 0, 成员不是const
    cout << is_volatile<decltype(p->i)>::value << endl; // 0, 成员不是volatile
}
// 编译选项:g++ -std=c++11 4-3-12.cpp
  • 而与auto相同的,decltype从表达式推导出类型后,进行类型定义时,也会允许一些冗余的符号。比如cv限制符以及引用符号&,通常情况下,如果推导出的类型已经有了这些属性,冗余的符号则会被忽略。
#include <type_traits>
#include <iostream>
using namespace std;
int i = 1;
int & j = i;
int * p = &i;
const int k = 1;
int main() {
    decltype(i) & var1 = i;
    decltype(j) & var2 = i;      // 冗余的&, 被忽略
    cout << is_lvalue_reference<decltype(var1)>::value << endl;// 1, 是左值引用
    cout << is_rvalue_reference<decltype(var2)>::value << endl;// 0, 不是右值引用
    cout << is_lvalue_reference<decltype(var2)>::value << endl;// 1, 是左值引用
    decltype(p)* var3 = &i;      // 无法通过编译
    decltype(p)* var3 = &p;      // var3的类型是int**
    auto* v3 = p;                  // v3的类型是int*
    v3 = &i;
    const decltype(k) var4 = 1; // 冗余的const,被忽略
    }
    // 编译选项:g++ -std=c++11 4-3-13.cpp

这里特别要注意的是decltype§*的情况。可以看到,在定义var3变量的时候,由于p的类型是int*,因此var3被定义为了int**类型。这跟auto声明中,*也可以是冗余的不同。在decltype后的*号,并不会被编译器忽略

4 追踪返回类型

4.1 追踪返回类型的引入
  • SUM函数第一版
template <typename T1, typename T2>
        double Sum(T1 & t1, T2 & t2) {
            auto s = t1 + t2;    // s的类型会在模板实例化时被推导出来
            return s;
        }
        int main() {
            int a = 3;
            long b = 5;
            float c = 1.0f, d = 2.3f;
            auto e = Sum<int ,long>(a, b);       // s的类型被推导为long
            auto f = Sum<float,float>(c, d);     // s的类型被推导为float
        }
        // 编译选项:g++ -std=c++11 4-2-7.cpp
  • SUM函数第二版
        // s的类型被声明为decltype(t1 + t2)
        template <typename T1, typename T2>
        void Sum(T1 & t1, T2 & t2, decltype(t1 + t2) & s) {
            s = t1 + t2;
        }
        int main() {
            int a = 3;
            long b = 5;
            float c = 1.0f, d = 2.3f;
            long e;
            float f;
            Sum(a, b, e);    // s的类型被推导为long
            Sum(c, d, f);    // s的类型被推导为float
        }
        // 编译选项:g++ -std=c++11 4-3-5.cpp
  • SUM函数第三版

错误的样例:

template <typename T1, typename T2>
        decltype(t1 + t2) Sum(T1 & t1, T2 & t2) {
            return t1 + t2;
        }

错误原因:编译器在推导decltype(t1 + t2)时的,表达式中的t1和t2都未声明(虽然它们近在咫尺,编译器却只会从左往右地读入符号)。

正确的样例:

template  < typename T1,  typename T2 >
        auto Sum(T1 & t1, T2 & t2)  -> decltype ( t1 + t2) {
            return t1 + t2;
        }
  • C++11引入新语法:追踪返回类型,来声明和定义这样的函数。原本函数返回值的位置由auto关键字占据。
  • auto->return_type 构成了追踪返回类型函数的两个基本元素。
4.2 使用追踪返回类型的函数
  • 简化函数的定义,提高代码的可读性。这种情况常见于函数指针中。
#include <type_traits>
#include <iostream>
using namespace std;
// 有的时候,你会发现这是面试题
int (*(*pf())())() {
  return nullptr;
}
// auto (*)() -> int(*) () 一个返回函数指针的函数(假设为a函数)
// auto pf1() -> auto (*)() -> int (*)() 一个返回a函数的指针的函数
auto pf1() -> auto (*)() -> int (*)() {
  return nullptr;
}
int main() {
  cout << is_same<decltype(pf), decltype(pf1)>::value << endl;     // 1
}
// 编译选项:g++ -std=c++11 4-4-3.cpp

//从外往里读
int(*(*pf())())()
int(*)() (*pf())()
(int(*)() (*)()) pf()
//还是用function对象吧
int(*(*pf())())()
int(*)() (*pf())()
function (*pf())()
function ()() pf()
function()> pf()

  • 广泛应用在转发函数中
#include <iostream>
using namespace std;
double foo(int a) {
    return (double)a + 0.1;
}
int foo(double b) {
    return (int)b;
}
template <class T>
    auto Forward(T t) -> decltype(foo(t)){
    return foo(t);
}
int main(){
    cout << Forward(2) << endl;      // 2.1
    cout << Forward(0.5) << endl;    // 0
}
// 编译选项:g++ -std=c++11 4-4-4.cpp

没有返回值的函数也可以被声明为追踪返回类型,程序员只需要将返回类型声明为void即可。

5 基于范围的for循环

for (auto e: arr)
            cout << e << '\t';
  • 能否使用基于范围的for循环,必须依赖于一些条件

    • for循环的迭代范围是可确定的
      对于类来说,如果该类有begin和end函数,那么begin和end之间就是for循环迭代的范围。对于数组而言,就是数组的第一个和最后一个元素间的范围。
    • 基于范围的for循环还要求迭代的对象实现++和==等操作符。
      对于标准库中的容器,如string、array、vector、deque、list、queue、map、set等,不会有问题,因为标准库总是保证其容器定义了相关的操作。普通的已知长度的数组也不会有问题。而用户自己写的类,则需要自行提供相关操作。
  • 反例

#include <iostream>
using namespace std;
int func(int a[]) {
    for (auto e: a)
        cout << e;
}
int main() {
    int arr[] = {1, 2, 3, 4, 5};
    func(arr);
}
// 编译选项:g++ -std=c++11 4-5-4.cpp

上述代码会报错,因为作为参数传递而来的数组a的范围不能确定,因此也就不能使用基于范围循环for循环对其进行迭代的操作。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值