BLOG

Record, summarize, and improve.

C++高级特性

构造函数

我们说的构造函数负责对象的数据成员的初始化,构造函数特点

  • 类对象的数据成员初始化
  • 为每个对象自动调用
  • 与类同名
  • 没有返回类型
  • 一个类可以有多个构造函数(函数重载)
  • 不能被声明为const,注:构造函数可以写const对象
    • 常量成员函数不能改变对象的状态,可以通过常量对象调用
    • 常量数据成员不能在构造函数中被初始化,只能通过构造函数初始化列表
  • X() = default; 自动生成函数体(可以自动生成默认构造函数,即没有输入参数的构造函数),X() = delete; 禁用X()函数,仅适用于类的特殊成员函数(构造函数、析构函数、拷贝构造函数以及拷贝赋值运算符),且该特殊成员函数没有默认参数

ConstRef::ConstRef( int& inI ): mI( inI ), mCi( inI ), mRi( inI ){}

class Employee{
  public:    
    Employee(); 
		...   
    void display() const;       
    void fire();   
};

void foo( const Employee& e) {
   e.display();  // OK. display() is a const member function
   e.fire();     // ERROR. fire() is not a const member function
}

int main() {
    Employee emp;
		...
    foo( emp );
    return 0;
}

必须在构造函数初始化列表中进行初始化的数据类型

  1. 对象数据成员没有默认构造函数。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
  2. const 成员或引用类型的数据成员。因为 const 对象或引用类型只能初始化,不能改变他们的值。
  3. 子类初始化父类private成员
#include <iostream>

using namespace std;

class Test
{
 public:
		Test (int){
		    cout <<"Test" << endl;
		};
 private:
    int x;
};

class Mytest 
{
 public:
    Mytest():test(1){       //初始化
		    cout << "Mytest" << endl;
    };
private:
    Test test; //声明
};

int _tmain(int argc, _TCHAR* argv[])
{
		Mytest test; 
		Test test2;  //error
		return 0;
}
class Test{
public:
    Test(){};
    Test (int x){ int_x = x;};
    void show(){cout<< int_x << endl;}
private:
    int int_x;
};

class Mytest:public Test{
public:
    Mytest() :Test(110){
      //Test(110);            // 构造函数只能在初始化列表中被显示调用,不能在构造函数内部被显示调用
    };
};

int _tmain(int argc, _TCHAR* argv[])
{
		Test *p = new Mytest();
		p->show();
		return 0;
}

初始化数据成员与对数据成员赋值的含义是什么?有什么区别?

  1. 内置数据类型,复合类型(指针,引用)在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
  2. 用户定义类型(class类型)结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)

总结:使用构造函数初始化列表,效率更高;没有在初始值列表中显示地初始化,所以实际是先进行了默认初始化,然后再进入构造函数体{}中执行赋值操作。const或者引用类型的数据成员的初始化只能通过构造函数初始化列表

委托构造函数

委托构造函数(Delegating Constructor)由 C++11 引入,是对 C++ 构造函数的改进,允许构造函数通过初始化列表调用同一个类的其他构造函数,目的是简化构造函数的书写,提高代码的可维护性,避免代码冗余膨胀。

通俗来讲,一个委托构造函数使用它所属的类的其他构造函数执行自己的初始化过程,或者说它把自己的一些(或者全部)职责委托给了其他构造函数。和其他构造函数一样,一个委托构造函数也有一个成员初始化列表和一个函数体,成员初始化列表只能包含一个其它构造函数,不能再包含其它成员变量的初始化,且参数列表必须与构造函数匹配。

class Foo
{
public:
	Foo() :type(4), name('x') {initRest();}
	Foo(int i) : type(i), name('x') {initRest();}
	Foo(char c) :type(4), name(c) {initRest();}

private:
	void initRest() {/* init othre members */}
	int type;
	char name;
	//...
};

使用委托构造函数后

class Foo
{
public:
    Foo() {initRest(); }
    Foo(int i) : Foo() {type = i;}
    Foo(char e) : Foo() {name = e;}
private:
    void initRest() { /* init othre members */}
    int type{1};
    char name{'a'};
};

一个委托构造函数想要委托另一个构造函数,那么被委托的构造函数应该包含较大数量的参数,初始化较多的成员变量。而且在委托其他构造函数后,不能再进行成员列表初始化,而只能在函数体内对其他成员变量进行赋值。

如果在委托构造函数中使用 try,可以捕获目标构造函数中抛出的异常

拷贝构造函数

拷贝构造函数(Copy Constructor)是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。

拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。
classname (const classname &obj) {
	// 构造函数的主体
}
Employee emp1(1, "Robert", "Black", 4000, true);
// called in one of the following cases:
Employee emp2( emp1 ); //copy-constructor called
Employee emp3 = emp2;  //copy-constructor called
void foo( Employee emp ); //copy-constructor called

如果在类中没有定义拷贝构造函数,编译器会自行定义一个,并按bit复制到新的object。

copy-and-swap idiom是解决方案,它优雅地帮助赋值运算符实现两件事:避免代码重复,并提供强大的异常保证

移动构造函数

移动构造函数(Move constructor),指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”

  • 参数的浅拷贝
  • 所有权转移到新对象(this)
  • 让参数(that)处于有效状态:destructor将被调用。
// constructor
string s=”apple”;
// copy constructor: s is an lvalue
string s1 = s;
// move constructor: right side is an rvalue
string s2 = s + s1;

数组通过移动构造函数移动:

#include <algorithm>
#include <iterator>
#include <vector>

class MyClass {
public:
    MyClass(std::vector<int>&& arr) :
data(std::make_move_iterator(std::begin(arr)), std::make_move_iterator(std::end(arr))) {
        // 移动构造函数体
    }

private:
    std::vector<int> data;
};

#include <array>

class MyClass {
public:
    MyClass(std::array<int, 5>&& arr) : data(std::move(arr)) {
        // 移动构造函数体
    }

private:
    std::array<int, 5> data;
};

class MyClass {
public:
    MyClass(int(&& arr)[10]) : data(std::move(arr)) {
        // 移动构造函数体
    }

private:
    int data[10];
};

所有的STL类都已经扩展以支持移动语义

拷贝构造函数和移动构造函数通常在以下情况下被调用:

  • 当一个对象被传递给一个函数时,会调用拷贝构造函数或移动构造函数来创建一个新的对象。
  • 当一个对象被返回时,会调用拷贝构造函数或移动构造函数来创建一个新的对象。
  • 当一个对象被初始化为另一个对象时,会调用拷贝构造函数或移动构造函数来创建一个新的对象。
#include <iostream>

class MyClass {
public:
    int* data;

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        data = new int(*other.data);
    }

    // 移动构造函数
    MyClass(MyClass&& other) {
        data = other.data;
        other.data = nullptr;
    }

    // 拷贝赋值符
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            delete data;
            data = new int(*other.data);
        }
        return *this;
    }

    // 移动赋值符
    MyClass& operator=(MyClass&& other) {
        if (this != &other) {
            delete data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }

    // 析构函数
    ~MyClass() {
        delete data;
    }
};

int main() {
    MyClass obj1;
    obj1.data = new int(1);
    MyClass obj2;
    obj2 = obj1; // 调用拷贝赋值符
    std::cout << *obj2.data << std::endl; // 输出 1
    MyClass obj3;
    obj3 = std::move(obj1); // 调用移动赋值符
    std::cout << *obj3.data << std::endl; // 输出 1
		MyClass obj4 = obj1; // 调用拷贝构造函数
		MyClass obj5 = std::move(obj1); // 调用移动构造函数
    return 0;
}

std::move() 函数主要用于移动对象的值,而不是移动内存块的内容。

在移动数组的情况下,使用 std::memcpy() 函数可以更加高效和直接地实现移动操作。

返回值优化

RVO

RVO(Return Value Optimization),是一种编译器优化技术,通过该技术,编译器可以减少函数返回时生成临时对象的个数,从某种程度上可以提高程序的运行效率,对需要分配大量内存的类对象其值复制过程十分友好。

当一个未具名且未绑定到任何引用的临时变量被移动或复制到一个相同的对象时,拷贝和移动构造可以被省略。当这个临时对象在被构造的时候,他会直接被构造在将要拷贝/移动到的对象。当未命名临时对象是函数返回值时,发生的省略拷贝的行为被称为RVO(返回值优化)。

RVO优化针对的是返回一个未具名对象,也就是说RVO的功能是消除函数返回时创建的临时对象

为了理解编译所做的优化,我们首先禁用RVO优化(可以通过-fno-elide-constructors来禁用RVO),输出如下:

#include <iostream>

class Test {
public:
    Test() { std::cout << "Default Constructor\n"; }
    Test(const Test&) { std::cout << "Copy Constructor\n"; }
    ~Test() { std::cout << "Destructor\n"; }
};

Test createTest() {
    Test t;
    return t;
}

int main() {
    Test t = createTest();
    return 0;
}

输出结果可能是

Default Constructor
Copy Constructor
Destructor
Destructor

现代C++编译器通常会自动应用RVO优化,下面的代码和之前的代码一样,但我们假设编译器进行了RVO优化。重新编译运行,输出如下:

Default Constructor
Destructor

由于RVO的优化,编译器会直接在main函数中的Test t对象的内存空间上构造返回的对象,而不是先构造一个临时对象再拷贝。这样就避免了拷贝构造函数的调用,从而减少了开销。

编译器明确知道函数会返回哪一个局部对象,那么编译器会把存储这个局部对象的地址和存储返回值临时对象的地址进行复用,也就是说避免了从局部对象到临时对象的拷贝操作,这就是RVO

NRVO

NRVO,又名具名返回值优化(Named Return Value Optimization),为RVO的一个变种,也是一种编译器对于函数返回值优化的方式。此特性从C++11开始支持,也就是说C++98、C++03都是没有将此优化特性写到标准中的,与RVO的不同之处在于函数返回的临时值是具名的。

NRVO与RVO的区别是返回的对象是具名的,既然返回的对象是具名的,那么对象是在return语句之前就构造完成。

我们仍然以一个例子来分析编译器的NRVO都做了哪些优化。

#include <iostream>

class Test {
public:
    Test() { std::cout << "Default Constructor\n"; }
    Test(const Test&) { std::cout << "Copy Constructor\n"; }
    ~Test() { std::cout << "Destructor\n"; }
};

Test createTest() {
    Test t;
    return t;
}

int main() {
    Test t = createTest();
    return 0;
}

通过-fno-elide-constructors禁用编译器优化之后,输出如下:

Default Constructor
Copy Constructor
Destructor
Destructor

在这个例子中,createTest 函数中创建了一个命名的局部变量 t。在没有NRVO优化的情况下,当函数返回时,t 会被拷贝到返回值对象中。这导致了一次额外的拷贝构造,增加了开销。

如果启用编译器返回值优化后,输出如下:

Default Constructor
Destructor

在这种情况下,由于NRVO的优化,编译器直接在调用 createTest 函数的地方构造返回的对象 t,而不再需要拷贝构造。因此,Copy Constructor 不会被调用,从而避免了不必要的对象拷贝和析构操作,提升了性能。

NRVO优化后,输出与RVO一致。

原理

从上述几节中,我们可以看到,编译器对返回值进行优化后,减少了很多不必要的函数调用开销,那么(N)RVO的原理到底是什么呢?

事实上,返回值优化的原理是将返回一个类对象的函数的返回值当做该函数的参数来处理。

为了能够更加清晰明了的分析编译器RVO和NRVO的优化机制,我们以下面代码为例,如下:

Obj fun() {
  Obj obj(1);
  return obj;
}

int main() {
  Obj obj = fun();
  return 0;
}

可能会有人有疑问,上面代码编译器是可以执行NRVO的,为什么还可以RVO呢?这是因为NRVO相比于RVO,是一种要求更为严格的优化方式,编译器启用NRVO的前提条件是返回值是具名的,但并不能说一段代码可以NRVO就不能RVO。

RVO原理

RVO优化的原理是消除函数返回时产生的一次临时对象。

正如<<深度探索C++对象模型>>中所述,编译器会将返回值函数的原型进行调整,编译器启用RVO优化,fun()函数会变成如下:

void fun(Obj &_obj) {
  Obj obj(1);
  _obj.Obj::Obj(obj); // 拷贝构造函数
  return;
}

而main函数内的调用则会变成:

int main() {
  Obj obj; // 仅定义不构造
  fun(obj);
  return 0;
}

经过上述转换,编译器将只调用一次构造函数和一次拷贝构造函数:

  • 构造函数:fun()函数中局部对象的构造
  • 拷贝构造函数:在fun()返回前用局部对象obj的值来拷贝构造传入的引用参数

经过上述优化,消除了为保存返回值而创建的临时对象以及将该局部对象拷贝给该临时对象的一次拷贝构造调用

虽然经过RVO优化后,性能有了部分提升,但是仍然存在一次拷贝构造,那么,在哪种场景下,RVO能够彻底优化呢?我们看下如下代码:

Obj fun() {
  return Obj(1);
}

int main() {
  Obj obj = fun();
  return 0;
}

编译会将fun函数优化为如下:

void fun(Obj &_obj) {
  _obj.Obj::Obj(1);
}

同样的,main函数会优化为如下:

int main() {
  Obj obj;
  fun(obj);
  
  return 0;
}

上述两种代码都进行了RVO优化,但是针对第一段代码,RVO的优化并没起什么作用,相反第二种代码块的优化优化的更彻底,彻底的消除了拷贝构造。

NRVO原理

在上面内容中,我们讲述了在对一开始的代码进行了RVO优化,但是并没有什么实质行的优化,那么,如果进行NRVO优化,编译器会将上述代码优化成什么样子呢?

fun函数会被优化成如下:

void fun(Obj &_obj) {
   _obj.Obj::Obj(1);
}

同样的,main函数会优化为如下:

int main() {
  Obj obj;
  fun(obj);
  
  return 0;
}

fun函数经过优化之后,去掉了RVO优化遗留的拷贝构造问题,达到了优化目标。

从上述代码可以看出,同样一块代码,RVO和NRVO的优化机制不同,得到的优化效果也不同。

编译器的优化,针对不同的场景,采取不同的优化方式,了解了这些,方便我们更好的写出更为高效的代码。

优化失效

在前面几节中,我们详细讲解了编译器在函数返回值中,启用(N)RVO进行优化,以提升程序性能,但是,编译器并非智能的,对于某些复杂场景或者特殊场景,是不会启用优化的,这个时候就需要开发人员依赖具体的情况,进行具体分析,以达到优化的目的。

运行时依赖(根据不同的条件分支,返回不同变量)

当编译器无法单纯通过函数来决定返回哪个实例对象时,会禁用(N)RVO。

代码如下:

Obj fun(bool flag) {
  Obj o1;
  Obj o2;
  if (flag) {
    return o1;
  }
  return o2;
}

int main() {
  Obj obj = fun(true);
  return 0;
}

程序输出如下:

in Obj()  0x7ffd8cbf7c00 // 构造o1对象
in Obj()  0x7ffd8cbf7bf0 // 构造o2对象
in Obj(const Obj &obj) 0x7ffd8cbf7c00 0x7ffd8cbf7c30 // 通过拷贝构造,将o1赋值给obj(main函数中的局部变量)
in ~Obj() 0x7ffd8cbf7bf0 // 析构o2对象
in ~Obj() 0x7ffd8cbf7c00 // 析构o1对象
in ~Obj() 0x7ffd8cbf7c30 // 析构obj

但是,下面这种情况例外,虽然其仍然依赖于具体的条件判断:

Obj fun(bool flag) {
  Obj obj;
  if (flag) {
    return obj;
  }
  obj.n = 10;
  return obj;
}

int main() {
  Obj obj = fun(true);
  return 0;
}

输出如下:

in Obj() 0x7ffd8cbf7bf0
in ~Obj() 0x7ffd8cbf7bf0

这是因为,对于单个对象以及多个函数出口的情况,编译器将多个出口优化为一个。

返回全局变量

当返回的对象不是在函数内创建的时候,是无法执行返回值优化的。

Obj g_obj;

Obj fun() {
  return g_obj;
}

int main() {
  Obj obj = fun();
  std::cout << &obj << std::endl;
  return 0;
}

编译并运行,结果如下:

in Obj()  0x6013b4 // 构造全局变量
in Obj(const Obj &obj) 0x6013b4 0x7ffc8abe14e0 // 构造main中的局部对象
0x7ffc8abe14e0
in ~Obj() 0x7ffc8abe14e0 // 析构main中的局部对象
in ~Obj() 0x6013b4 // 析构全局变量
返回函数参数

与返回全局变量类似,当返回的对象不是在函数内创建的时候,是无法执行返回值优化的。

代码如下:

Obj fun(Obj obj) {
  return obj;
}

int main() {
  Obj o;
  Obj obj = fun(o);
  std::cout << "in main " << &obj << std::endl;
  return 0;
}

编译并运行之后,输出:

in Obj() 0x7ffdbb43da00
in Obj(const Obj &obj) 0x7ffdbb43da00 0x7ffdbb43da10
in Obj(const Obj &obj) 0x7ffdbb43da10 0x7ffdbb43d9f0
in ~Obj() 0x7ffdbb43da10
in main 0x7ffdbb43d9f0
in ~Obj() 0x7ffdbb43d9f0
in ~Obj() 0x7ffdbb43da00
返回成员变量

在某些特殊情况下,即使是未具名变量,也不能RVO。

代码如下:

struct Wraper {
   Obj obj;
 };

Obj fun() {
  return Wraper().obj;
}

int main() {
  Obj obj = fun();
  std::cout << &obj << std::endl;
  return 0;
}

编译并运行,结果如下:

in Obj()  0x7ffed7f85290 // 构造Wraper中的obj对象
in Obj(const Obj &obj) 0x7ffed7f85290 0x7ffed7f852c0 // 通过拷贝赋值给main函数中的局部变量
in ~Obj() 0x7ffed7f85290 // 析构Wraper中的obj对象
0x7ffed7f852c0
in ~Obj() 0x7ffed7f852c0 // 析构main中的局部对象
存在赋值行为

(N)RVO只能在从返回值创建对象时发送,在现有对象上使用operator=而不是拷贝/移动构造函数,这样是不会进行RVO操作的。

代码如下:

Obj fun() {
  return Obj();
}

int main() {
  Obj obj;
  obj = fun();
  return 0;
}

编译并运行之后,输出:

in Obj()  0x7ffd7a100cf0 // 构造main()函数中的obj对象
in Obj()  0x7ffd7a100d00 // 生成临时对象
in operator=(const Obj &obj) 0x7ffd7a100d00 0x7ffd7a100cf0 // 调用赋值函数,将临时变量赋值给目标(main函数中的局部对象obj)
in ~Obj() 0x7ffd7a100d00 // 析构临时对象
in ~Obj() 0x7ffd7a100cf0 // 析构main中的局部对象
使用std::move()返回

在返回值上调用std::move()进行返回是一种错误的方式。它会尝试强制调用移动构造函数,但这样会导致RVO失效。因为即使没有显示调用std::move(),编译器优化中也会执行move操作。

代码如下:

Obj fun() {
  Obj obj;
  return std::move(obj);
}

int main() {
  Obj obj = fun();
  return 0;
}

输出如下:

in Obj()  0x7ffe7d4d1720
in Obj(const Obj &&obj)
in ~Obj() 0x7ffe7d4d1720
0x7ffe7d4d1750
in ~Obj() 0x7ffe7d4d1750

从上面输出可以看出,与不使用std::move()返回相比,使用std::move()返回增加了一次拷贝构造调用和一次析构调用。

经验之谈

之前看过一篇文章,也是(N)RVO相关的,当时作者观点是优化依赖于编译器,而作者同事的观点则是依赖代码实现来实现优化,当时给出的例子如下:

class BigObject {
// 定义
};

BigObject fun() {
  BigObject obj;
  // do sth
  return obj;
}

是不是似曾相识?类似于我们前面NVRO中的代码示例。

作者的观点是此种代码满足编译器NVRO优化的条件,所以不需要优化;而作者同事的观点则是,不能严格依赖编译器,所以作者同事的优化建议如下:

void fun(BigObject &obj) {
  // do sth with obj
}

在此,我说下自己的观点吧:

  1. 代码优化不应该依赖编译器,因为无法保证在其他编译器下就能得出跟当前类似的优化效果
  2. 依赖编译器优化的前提是开发人员了解编译器的优化机制或者说开发人员知道写怎样的代码能达到编译器优化的标准,但是,如果部门入职的新人接手了这块工作,在开发过程中,发现老代码都是直接返回(也就是作者口中的编译器优化),然后新人也直接返回(即使编译器不会进行优化,例如前面的返回全局变量等),这样恰恰得到相反的结果

所以,总的来说,我倾向于作者同事的优化方案。对于char、int、double等元类型,在函数中直接返回;而对于需要返回struct、class类型的函数,则直接作为函数入参,在函数内部进行初始化。

当然了,上面仅仅是我的个人观点,至于使用编译器优化还是上述引用传参的方式,则依赖于开发者的个人喜好或者团队的代码风格。但是需要注意的是,如果使用编译器优化,则需要小心小心再小心,否则就会导致事倍功半的效果,进而导致程序性能损失

结语

(N)RVO是编译器对于函数返回值的一种优化技术,旨在消除临时对象的创建。了解编译器的优化,可以提升我们的程序运行效率,但是需要注意的是,如果单纯依赖编译器优化,可能会导致某些我们意想不到的情况发生。所以,在使用编译器优化方式之前,我们需要保证代码的实现方式能够启用RVO优化。

Lvalues 和 Rvalues (C++)

每个 C++ 表达式都有一个类型,属于值类别。 值类别是编译器在表达式计算期间创建、复制和移动临时对象时必须遵循的规则的基础。

Lvalues:

  • 参考源代码中可以访问到的多个对象
    • 有名字的objects
    • 通过指针/引用访问的对象
  • Lvalues 不能被移动

Rvalues:

  • 参考源代码中仅有的一个可访问对象
    • 临时对象(例如按值函数返回)
  • Rvalues可以被移动

左值(lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象,lvalue 具有程序可访问的地址。 例如,lvalue 表达式包括变量名称,其中包括 const 变量、数组元素、返回 lvalue 引用的函数调用、位域、联合和类成员。

右值(rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象,表示在下一个分号处销毁的临时对象(更准确地说:在词汇上包含右值的完整表达式的末尾)

C++17 标准对表达式值类别的定义如下:

  • glvalue 是一个表达式,它的计算可以确定对象、位域或函数的标识。glvalue(generalized lvalue)代表“广义左值”,即可以出现在赋值语句左侧的表达式或者取地址符 & 右侧的表达式。glvalue 可以是一个对象或者函数,也可以是一个数组或指针的元素,或者是一个类成员的引用。
  • 纯右值(prvalue, pure rvalue),纯粹的右值,没有标识符、不可以取地址的表达式, 要么是纯粹的字面量,例如 10, true;要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。prvalue 表达式没有可供程序访问的地址。 例如,prvalue 表达式包括文本、可返回非引用类型的函数调用,以及在表达式计算期间创建的但只能由编译器访问的临时对象。
  • 将亡值(xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念(因此在传统 C++中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值,xvalue 表达式有一个地址,该地址不再可供程序访问,但可用于初始化 rvalue 引用,以提供对表达式的访问。 例如,它包括可返回 rvalue 引用的函数调用,以及数组下标、成员和指向其中数组或对象是 rvalue 引用的成员表达式的指针。
  • lvalue 是 glvalue 的一种特殊情况,即具有“可修改左值性质”的表达式。lvalue 是一个非 xvalue 的 glvalue。
  • rvalue 是一个 prvalue 或 xvalue。

下图阐释了各类别之间的关系:

Image in a image block

因此,可以将 glvalue 和 prvalue 分为左值和右值两类,而 xvalue 则是右值的一种特殊情况,同时具有右值引用的属性。左值可以被取地址,右值不能被取地址,但可以被移动。

xvalue可能稍有些难以理解,我们来看这样的代码:

std::vector<int> foo() {
    std::vector<int> temp = {1, 2, 3, 4};
    return temp;
}

std::vector<int> v = foo();

就传统的理解而言,函数 foo 的返回值 temp 在内部创建然后被赋值给 v, 然而 v 获得这个对象时,会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大, 这将造成大量额外的开销(这也就是传统 C++ 一直被诟病的问题)。

在最后一行中,v 是左值、 foo() 返回的值就是右值(也是纯右值)。但是,v 可以被别的变量捕获到, 而 foo() 产生的那个返回值作为一个临时值,一旦被 v 复制后,将立即被销毁,无法获取、也不能修改。 而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。

在 C++11 之后,编译器为我们做了一些工作,此处的左值temp会被进行此隐式右值转换, 等价于static_cast<std::vector<int> &&>(temp),进而此处的v会将foo局部返回的值进行move。 也就是后面我们将会提到的move语义。

glvalue:

  • 对象或函数的名称,例如变量、函数、类成员等。
  • 对象或函数的指针,例如指向变量、函数、类成员的指针等。
  • 对象或函数的引用,例如左值引用、右值引用等。
  • 数组或函数的名称,例如数组名、函数名等。
  • 表达式中的子对象,例如成员访问表达式、下标表达式等。
  • typeid 表达式的结果。
  • sizeof 表达式的结果。
  • noexcept 表达式的结果。
  • 一些运算符表达式的结果,例如取地址运算符、解引用运算符、逗号运算符、条件运算符等。
  • 类型转换表达式的结果,例如 static_cast、reinterpret_cast、const_cast 等。
  • 一些流运算符表达式的左操作数,例如输入流运算符、输出流运算符等。

lvalue:

  • 变量和函数的名称,例如 int x 和 void f()
  • 对象和函数的指针,例如 int* p 和 void (*pf)()
  • 对象和函数的引用,例如 int& r 和 void (&rf)()
  • 左值表达式,例如 ++x 和 x = y
  • 数组和函数的名称,例如 int a[] 和 void g()
  • 表达式中的子对象,例如成员访问表达式、下标表达式等。
  • 函数调用表达式,返回左值引用类型的函数调用表达式是 lvalue。
  • 重载运算符表达式,返回左值引用类型的重载运算符表达式是 lvalue。
  • 通过类型转换得到的 lvalue,例如 static_cast<int&>(x)
  • 通过 decltype 得到的 lvalue,例如 decltype(x)& r = x
  • 通过 typeid 得到的 lvalue,例如 const std::type_info& ti = typeid(int)

prvalue:

  • 字面量,如整数、浮点数、字符和字符串字面量。
  • 临时对象,如通过调用构造函数创建的对象。
  • 返回右值引用的函数调用。
  • 类型转换表达式,如 static_cast、reinterpret_cast 和 const_cast。
  • 表达式中的运算符,如算术运算符、位运算符、逻辑运算符和条件运算符。
  • Lambda 表达式。

xvalue:

  • 通过 std::move 函数将左值转换为右值引用的表达式。
  • 返回右值引用的函数调用。
  • 转换为右值引用的类型转换表达式,例如 static_cast<int&&>(x)reinterpret_cast<int&&>(p)const_cast<int&&>(x) 等。
  • 表达式中的子对象,例如成员访问表达式、下标表达式等,如果其结果是右值引用类型。
  • 通过 lambda 表达式产生的函数对象的调用运算符表达式。

示例

以下示例演示左值和右值的多种正确的和错误的用法:

// lvalues_and_rvalues2.cpp
int main()
{
    int i, j, *p;

    // Correct usage: the variable i is an lvalue and the literal 7 is a prvalue.
    i = 7;

    // Incorrect usage: The left operand must be an lvalue (C2106).`j * 4` is a prvalue.
    7 = i; // C2106
    j * 4 = 7; // C2106

    // Correct usage: the dereferenced pointer is an lvalue.
    *p = i;

    // Correct usage: the conditional operator returns an lvalue.
    ((i < 3) ? i : j) = 7;

    // Incorrect usage: the constant ci is a non-modifiable lvalue (C3892).
    const int ci = 7;
    ci = 9; // C3892
}

备注

此主题中的示例阐释了未重载运算符时的正确和错误用法。 通过重载运算符,可以使表达式(如 j * 4)成为左值。

在提到对象引用时,通常会使用术语 lvalue 和 rvalue。 有关引用的详细信息,请参阅 lvalue 引用声明符:& 和 Rvalue 引用声明符:&&

左值引用和右值引用

在明确了左值和右值的关系之后,对于左值的引用就是左值引用,而对于右值的引用就是右值引用。如果一个表达式的类型是一个lvalue reference (例如, T& 或 const T&, 等.),那这个表达式就是一个lvalue。其它情况,这个表达式就是一个rvalue。

C++11之前的引用,我们指的是左值引用(T&),即:

int a = 3;
int& ar = a;  //正确的引用
int& aar = 3; //错误,我们需要让引用指向一个左值

C++11引入了右值引用的概念,使我们可以取一个右值的引用(T&&):

int&& a = 3;  //正确
int&& b = MyInt();  //正确

C++11 提供了std::move这个方法将左值参数无条件的转换为右值, 有了它我们就能够方便的获得一个右值临时对象,对对象类型右值引用的转换。

int main()
{
    static_cast<int&&>(7); // 表达式static_cast<int&&>(7)属于xvalue类别,
													 // 因为它是对对象类型的右值引用的强制转换
    std::move(7);          // std::move(7)等价于static_cast<int&&>(7)

    return 0;
}

需要拿到一个将亡值,就需要用到右值引用的申明T &&,其中T是类型。 右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。

当一个函数接受一个左值引用参数时,它可以接受一个左值或一个右值。

  • 当传递一个左值时,该左值将被绑定到左值引用参数上。这意味着该函数可以修改传递给它的变量的值,因为它接受的是该变量的引用。
  • 当传递一个右值时,右值将被转换为一个临时对象,并将其绑定到左值引用参数上。这个临时对象的生命周期将持续到函数调用结束。这意味着该函数可以使用该临时对象的值,但不能修改传递给它的变量的值,因为它只是该变量的一个临时副本。
  • 接受左值引用参数的函数只能接受左值,不能接受右值。将一个左值传给一个左值引用参数的函数实际上传入的是左值引用。

当一个函数接受一个右值引用参数时,它可以接受一个左值或一个右值,但是通常情况下,右值引用参数更适合接受右值。

  • 当传递一个左值时,该左值将被转换为一个右值引用,这个右值引用将绑定到该左值的临时对象上。这个临时对象的生命周期将持续到函数调用结束。这使得函数可以修改该临时对象的值,但不能修改传递给它的变量的值,因为它只是该变量的一个临时副本。
  • 当传递一个右值时,该右值将直接绑定到右值引用参数上,不需要进行任何转换。这使得函数可以使用该右值的值,但不能修改传递给它的变量的值,因为它只是该变量的一个临时副本。
  • 接受右值引用参数的函数只能接受右值,不能接受左值。将一个右值传给一个右值引用参数的函数实际上传入的是右值引用。

下面是一个示例,展示了一个接受左值引用参数的函数被右值引用的函数调用的情况:

#include <iostream>
void foo(int& x) {
    std::cout << "foo(int&): x = " << x << std::endl;
}

void bar(int&& x) {
    std::cout << "bar(int&&): x = " << x << std::endl;
    foo(x);  // 右值引用的函数调用左值引用的函数
}

int main() {
    int x = 42;
    bar(std::move(x));  // 传递一个右值
    return 0;
}

在这个示例中,bar 函数接受一个右值引用参数 x,并将其传递给了 foo 函数,foo 函数接受一个左值引用参数。在 bar 函数中,我们使用了 std::move 将 x 转换为一个右值。即使 x 是一个左值,它仍然可以被传递给接受左值引用的函数 foo,因为它被转换为一个临时对象,并将其绑定到左值引用参数上。

move语义

Rvalue 引用支持“move语义”的实现,这可以显著提高应用程序的性能。 利用移动语义,你可以编写将资源(如动态分配的内存)从一个对象转移到另一个对象的代码。 move语义很有效,因为它允许从临时对象(无法在程序中的其他位置引用)转移资源。

若要实现move语义,通常可以向类提供“移动构造函数”,或者提供移动赋值运算符 (operator=)。 其源是右值的复制和赋值操作随后会自动利用移动语义。与默认复制构造函数不同,编译器不提供默认移动构造函数。

传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。 右值引用的出现恰好就解决了这两个概念的混淆问题,为了结合左值引用来轻易完成move语义的实现。什么是move语义,为什么需要move语义,我们来举一个std::vector的栗子。我们在执行v2=v1时,需要先完成一次函数调用,即调用拷贝赋值运算符,然后执行内存分配,最后循环逐个元素

如果v1和v2我们都需要的话,生成两份拷贝自然是没有问题的,但多数情况下我们只希望使用v2,那么我们就只希望生成一份拷贝,减少不必要又麻烦的拷贝过程:设想如果它包含10000个元素,要增加多大的开销?例如下面的swap函数:

template <class T> swap(T& a, T& b){
  T tmp(a);  // a 复制到 tmp
  a = b;     // b 复制到 a
  b = tmp;   // tmp 复制到 b
}

//试试更好的方法,不会生成额外的拷贝
template <class T> swap(T& a, T& b){
  T tmp(std::move(a)); // a 移动到 tmp
  a = std::move(b);    // b 移动到 a
  b = std::move(tmp);  // tmp 移动到 b
}

move函数所做的只是拿到一个左值或右值参数,然后都将其返回为右值同时不触发任何拷贝函数。它的作用就是就是相当于把参数的值剪切到目标对象的值,move可以说是一种具有破坏性的读操作。

std::string str = "Hello";
std::vector<std::string> v;

v.push_back(str);
std::cout<<"After copy, str is \""<<str<<\"\n;
//输出结果为 After copy, str is "Hello"

v.push_back(std::move(str));
std::cout<<"After move, str is \""<<str<<\"\n;
//输出结果为 After move, str is ""

放在v2=v1里,我们能让参数v1的体积为0,只生成v2,避免生成额外拷贝。要实现这样移值的move语义,右值引用的好处就来了,它使我们能轻松快速地完成这个功能:

template <class T>
typename remove_reference<T>::type&&  //去掉引用,再变为右值引用作为返回值类型
move(T&& a){
  return a;  //通过引用直接把右值传给目标
}

C++11也引入了使用move语义来实现的移动构造函数的概念。相比C++98的拷贝构造函数,其区别就相当于剪切粘贴和复制粘贴,就像上面的例子一样。

void someFunc(Widget w);
Widget wid;
someFunc(wid);  //w是由wid拷贝构造
someFunc(std::move(wid));  //w是由wid移动构造

比如:

#include <iostream>
class A {
public:
    int *pointer;
    A():pointer(new int(1)) { 
        std::cout << "构造" << pointer << std::endl; 
    }
    A(A& a):pointer(new int(*a.pointer)) { 
        std::cout << "拷贝" << pointer << std::endl; 
    } // 无意义的对象拷贝
    A(A&& a):pointer(a.pointer) { 
        a.pointer = nullptr;
        std::cout << "移动" << pointer << std::endl; 
    }
    ~A(){ 
        std::cout << "析构" << pointer << std::endl; 
        delete pointer; 
    }
};
// 防止编译器优化
A return_rvalue(bool test) {
    A a,b;
    if(test) return a; // 等价于 static_cast<A&&>(a);
    else return b;     // 等价于 static_cast<A&&>(b);
}
int main() {
    A obj = return_rvalue(false);
    std::cout << "obj:" << std::endl;
    std::cout << obj.pointer << std::endl;
    std::cout << *obj.pointer << std::endl;
    return 0;
}

在上面的代码中:

  1. 首先会在 return_rvalue 内部构造两个 A 对象,于是获得两个构造函数的输出;
  2. 函数返回后,产生一个将亡值,被 A 的移动构造(A(A&&))引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了 obj 中,而将亡值的指针被设置为 nullptr,防止了这块内存区域被销毁。

最后我们再来看一个比较容易混淆的左值和右值问题,下面的rhs是左值还是右值?

class Widget{
  public:
    Widget(Widget&& rhs);
  ......
};

答案是左值!区别左值和右值的最根本标准就是,能否取到地址,上面我们显然可以对其进行取址操作,因此它是一个左值。其次,即使这个参数标明的类型是T&&,它其实是通用引用(universal reference)而不一定是右值引用。标为T&&类型的变量既可以是右值引用,即只能引用右值,也可以是通用引用,既可以引用左值,也可以引用右值。

在 C++ 中,将一个左值拷贝到另一个左值时,会触发拷贝构造函数或拷贝赋值运算符,而不是移动构造函数或移动赋值运算符。将一个左值拷贝到另一个左值时,通常会触发一次拷贝构造函数或一次赋值操作符重载函数。拷贝构造函数用于创建一个新的对象,该对象是通过复制另一个对象而创建的。当你将一个对象作为参数传递给函数或从函数中返回一个对象时,拷贝构造函数会被调用。赋值操作符重载函数用于将一个已经存在的对象的值复制到另一个已经存在的对象中。当你将一个对象赋值给另一个对象时,赋值操作符重载函数会被调用。

将一个右值拷贝到另一个右值通常只会触发一次拷贝构造函数调用,因为右值是临时对象,它们没有名称,所以在拷贝时可以直接将其内部数据移动到目标对象中,而不需要进行深拷贝。

将一个右值拷贝到另一个左值通常会触发一次拷贝构造函数或一次移动构造函数。当右值被拷贝到左值时,编译器会首先尝试使用移动构造函数来完成该操作。如果没有移动构造函数可用,编译器会使用拷贝构造函数来完成操作。

将一个左值拷贝到另一个右值,会触发移动构造函数或移动赋值运算符,而不是拷贝构造函数或拷贝赋值运算符。在 C++11 中引入了右值引用,它可以绑定到右值,并且可以通过 std::move() 转换为右值引用,从而触发移动语义。当将一个左值传递给一个函数,而该函数的参数类型为右值引用时,编译器会自动将左值转换为右值引用,从而触发移动语义。

#include <iostream>

class MyClass {
public:
    MyClass() {
        std::cout << "Default constructor" << std::endl;
    }

    MyClass(const MyClass& other) {
        std::cout << "Copy constructor" << std::endl;
    }

    MyClass(MyClass&& other) {
        std::cout << "Move constructor" << std::endl;
    }

    MyClass& operator=(const MyClass& other) {
        std::cout << "Copy assignment operator" << std::endl;
        return *this;
    }

    MyClass& operator=(MyClass&& other) {
        std::cout << "Move assignment operator" << std::endl;
        return *this;
    }
};

int main() {
	  //right -> left
    MyClass a;
    MyClass b = std::move(a);  // move constructor called
    MyClass c = b;  // copy constructor called
    MyClass d;
    d = std::move(c);  // move assignment operator called
    MyClass e;
    e = d;  // copy assignment operator called
	
	  //left -> left
	  MyClass a;
    MyClass b = a; // Copy constructor called
    MyClass c = std::move(a); // Move constructor called
    b = c; // Copy assignment operator called
    b = std::move(c); // Move assignment operator called
	  MyClass obj4 = MyClass(); // Default constructor called, then copy constructor called
    MyClass obj5;
    obj5 = MyClass(); // Default constructor called, then assignment operator called
	
	  //right -> right
	  MyClass a;
    MyClass b(std::move(a));
    MyClass c = std::move(b);
	
	  //left -> right
	  MyClass obj1;
    MyClass obj2 = obj1; // Copy Constructor
    MyClass obj3 = std::move(obj1); // Move Constructor
    obj3 = obj2; // Copy Assignment Operator
    obj3 = std::move(obj2); // Move Assignment Operator
    
    return 0;
}

转发引用(通用引用)

类型声明当中的“&&”有的时候意味着rvalue reference,但有的时候意味着rvalue reference或者lvalue reference。这种灵活的引用方式,称之为universal references(万能引用或转发引用、通用引用)。

template<typename T>
void f(std::vector<T>&& param);  // here, “&&” means rvalue reference
 
template<typename T>
void f(T&& param);               // here, “&&”does not mean rvalue reference

Rvalue references只能绑定到右值上,lvalue references除了可以绑定到左值上,在某些条件下还可以绑定到右值上

这里某些条件绑定右值为:常左值引用绑定到右值,非常左值引用不可绑定到右值!例如:

string &s = "asd";  // error
const string &s = "asd";  // ok

// 规则简化如下:
// 左值引用   {左值}  
// 右值引用   {右值}
// 常左值引用  {右值}

string f() { return "abc"; }

void g() {
    const string &s = f();       // still legal?
    cout << s << endl;
}

上面g函数中合法?

答案是合法的,原因是s是个左值,类型是常左值引用,而f()是个右值,前面提到常左值引用可以绑定到右值!所以合法,当然把const去掉,便是不合法!

和所有的引用一样,你必须对universal references进行初始化,而且正是universal reference的initializer决定了它到底代表的是lvalue reference 还是 rvalue reference:

  • 如果用来初始化universal reference的表达式是一个左值,那么universal reference就变成lvalue reference。
  • 如果用来初始化universal reference的表达式是一个右值,那么universal reference就变成rvalue reference。

例如:

Widget&& var1 = someWidget;      // here, “&&” means rvalue reference
auto&& var2 = var1;              // here, “&&” does not mean rvalue reference

可以对var1取址,所以var1是一个lvalue。var2的类型声明是auto&&,所以它是一个universal reference,并且因为它会被var1(一个lvalue)初始化,进而,var2就变成了一个lvalue reference。如果草草略过这段代码,你可能就会以为var2是rvalue reference, 类型声明当中的 "&&" 会误导你得出这个结论。但实际上,当一个universal reference开始被lvalue初始化的时候,var2就变成了lvalue reference。就好像我们是这么声明var2的:

Widget& var2 = var1;

正如前面所说的,如果一个表达式的类型是lvalue reference,它就是lvalue。又比如:

std::vector<int> v;
auto&& val = v[0];               // val becomes an lvalue reference

val 是universal reference,并且被v[0]初始化,即是说用调用std::vector<int>::operator[]的结果来初始化。这个函数返回vector元素的lvalue reference。因为所有的lvalue references都是lvalues,并且这个lvalue被用来初始化val,val就变成了lvalue reference,即使它的类型声明看起来像是rvalue reference。

Universal reference在函数模板的参数中最常见。比如:

template<typename T>
void f(T&& param);               // “&&” might mean rvalue reference

调用:

f(10);                           // 10 is an rvalue
int x = 10;
f(x);                            // x is an lvalue

第一行:param 被字面值10 初始化,因为你不能对字面值取址,所以10是一个rvalue。这就意味着上面对f的调用当中,universal reference param被一个rvalue初始化,所以 param 就变成了rvalue reference – 具体来讲,就是 int&&

第三行:param被变量 x 初始化,因为你能对 x 取址,所以x是个lvalue。这就是说,这里对f的调用,universal reference param被一个lvalue初始化,因此param就变成lvalue reference – 准确的说,就是int&

只有在发生类型推导的时候 “&&” 才代表 universal reference 吗。如果没有类型推导,就没有universal reference。这种时候,类型声明当中的“&&”总是代表着rvalue reference。

template<typename T>
void f(T&& param);               // deduced parameter type ⇒ type deduction;
                                 // && ≡ universal reference
 
template<typename T>
class Widget {
    ...
    Widget(Widget&& rhs);        // fully specified parameter type ⇒ no type deduction;
    ...                          // && ≡ rvalue reference
};

在每一个例子当中,如果你看到 T&& (其中T是模板参数),那这里就有类型推导,那 T&& 就是 universal reference如果你看到 “&&” 跟在一个具体的类型名后面 (e.g.,Widget&&),那它就是个 rvalue reference。

完美转发

讲了这么多左值右值和move语义,C++11正是利用它们解决了C++98解决不了的完美转发(perfect forwarding)问题,即实参被传入到函数中,当它被再传到另一个函数中,它依然是一个左值或右值,我们来看一个例子:

template <class T>
void f2(T t){
  cout<<"f2"<<endl;
}

template <class T>
void f1(T t){
  cout<<"f1"<<endl;
  f2(t);  //如果t是右值,我们希望传入f2也是右值
}         //如果t是左值,我们希望传入f2也是左值

//在main函数里:
int a = 2;
f1(3); //传入右值
f1(a); //传入左值

C++11之前的情况是怎么样的呢?当我们从f1调用f2的时候,不管传入f1的是右值还是左值,因为t是一个变量名,传入f2的时候都变成了左值,这就会造成因为调用T的拷贝构造函数而生成不必要的拷贝浪费大量资源,我们来看C++11如何解决这个问题:

template <class T>
void f2(T t){
  cout<<"f2"<<endl;
}

template <class T>
void f1(T&& t){    //这是通用引用,而不是右值引用
  cout<<"f1"<<endl;
  f2(std::forward<T>(t));  //std::forward<T>(t)用来把t转发为左值或右值,决定于T
}

这样当从f1调用f2的时候,调用的就是移动构造函数而不是拷贝构造函数,实现了完美转发,减少了资源浪费。

function object

类模版std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等。std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)。

用函数对象(仿函数)代替函数指针有几个优点:

  • 首先,因为对象可以在内部修改而不用改动外部接口,因此设计更灵活,更富有弹性。函数对象也具备有存储先前调用结果的数据成员,函数对象可包含状态。在使用普通函数时需要将先前调用的结果存储在全程或者本地静态变量中,但是全程或者本地静态变量需要将数据回写到静态存储区,容易出现写没有完成从而读到错误数据,函数对象则不需要将数据写到静态存储区,所以不会出现这种现象,相当于用局部变量的方式实现了全局变量的功能。
  • 其次,在函数对象中编译器能实现内联调用,从而更进一步增强了性能。这在函数指针中几乎是不可能实现的。
  • 函数对象是一个类型,可用作模版参数
function与普通函数
int Func(int a, int b)
{
    return a + b;
}

int main(int argc, char** argv)
{
    std::function<int(int, int)> func = Func;
    func(1, 2);
    return 0;
}
function与仿函数
struct Func
{
public:
    int operator() (int a, int b)
    {
        return a + b;
    }
};

int main(int argc, char** argv)
{
    Func func;
    std::function<int(int, int)> f = func;
    func(1, 2);
}
function与Lambda函数
int main(int argc, char** argv)
{
    auto fun = [](const int a, const int b) {return a + b; };
    std::function<int(int, int)> func = fun;
    fun(1, 2);
    return 0;
}
function与类的静态成员函数
struct Func
{
    static int Add(int a, int b)
    {
        return a + b;
    }
};

int main(int argc, char** argv)
{
    function<int(int, int)> f = &Func::Add;
    f(1, 2);
    return 0;
}
function与bind函数调用类成员函数
struct Func
{
    int Add(int a, int b)
    {
        return a + b;
    }
};

int main(int argc, char** argv)
{
    Func f;
    //指针形式调用成员函数
    function<int(int, int)> f1 = bind(&Func::Add, &f, _1, _2);
    f1(1, 2);
    //对象形式调用成员函数
    function<int(int, int)> f2 = bind(&Func::Add, f, _1, _2);
    f1(1, 2);
    return 0;
}
function调用共有数据成员
struct Func
{
    Func(int num) : m_num(num) {}
    int Add(int a, int b)
    {
        return a + b;
    }
    int m_num;
};

int main(int argc, char** argv)
{
    Func f(2);
    function<int(const Func&)> f1 = &Func::m_num;
    std::cout << f1(f) << std::endl;    //2return 0;
}
function与类模板和函数模板
template<typename T>
struct Func
{
    static int Add(T a, T b){ return a + b; }
    int Add2(T a, T b){ return a + b; }
};

template<typename T>
T Add(T a, T b)
{
    return a + b;
}

int main()
{
    //类模板
    Func<int> f1;
    function<int(int, int)> f2 = bind(&Func<int>::Add2, &f1, _1, _2);
    function<int(int, int)> f3 = &Func<int>::Add;
    //函数模板
    function<int(int, int)> f4 = Add<int>;
    return 0;
}

function object adapter

  • 将function object的参数绑定到某个具体值身上,从而使二元function object转化成一元function object,标准库提供了两个binder adapter:bind1st和bind2nd,分别将指定值绑定到第一和第二个参数上
  • 为了支持多种容器,传入一对iterator用以标示出一组元素范围,为了支持多种元素型别需要将元素型别参数化,作用于元素身上的操作也需要参数化,以同时支持函数指针和function object两种方式
  • class内的member function会自动视为inline function
  • “成员逐一初始化”的行为模式有问题时(一个object赋给另一个object,两个object中的成员有可能指向同一块内存,释放时会出错),需要提供一个copy constructor进行正确的初始化操作
#include <iostream>
#include <functional>

// 定义一个加法仿函数
struct Add {
    int operator()(int a, int b) const {
        return a + b;
    }
};

int main() {
    // 使用 std::function 封装 Add 仿函数
    std::function<int(int, int)> func = Add();
    
    // 调用封装的仿函数
    int result = func(3, 5);
    std::cout << "3 + 5 = " << result << std::endl; // 输出 3 + 5 = 8

    return 0;
}

RAII

定义

RAII是一种使用在面向对象语言中的资源(内存,互斥锁,或者文件描述符)管理机制,使用RAII的语言中,最出名的当属C++和RUST。对C++来说,许多公司已经开始禁用裸指针(强制使用基于RAII的智能指针)来避免内存泄漏。而RUST,正是因为强制RAII机制使得其拥有了绝对的内存安全。Resource Acquisition Is Initialisation, 顾名思义,意味着任何资源的获取都应该发生在类的构造函数中,资源的释放应发生在析构函数中,这意味着所有资源的life cycle都与一个 object紧紧绑定在一起。

problem:

struct resource
{
    resource(int x, int y) { cout << "resource acquired\n"; }
    ~resource() { cout << "resource destroyed\n"; }
};

void func()
{
    resource *ptr = new resource(1, 2);
    int x;
    std::cout << "Enter an integer: ";
    std::cin >> x;
    if (x == 0)
        throw 0; // the function returns early, and ptr won't be deleted!
    if (x < 0)
        return; // the function returns early, and ptr won't be deleted!
    // do stuff with ptr here
    delete ptr;
}

在上面的代码中,早期 return 或 throw 语句会导致函数终止而 ptr 不被删除。因此,为变量  ptr 分配的内存 , 现在会泄漏(并且每次调用此函数并提前返回时都会再次泄漏)。

solution:

template<class T>
class smart_ptr
{
    T* m_ptr;
public:
    template<typename... Args>
    smart_ptr(Args&&... args) : m_ptr(new T(std::forward<Args>(args)...)){}
    ~smart_ptr() { delete m_ptr; }

    smart_ptr(const smart_ptr& rhs) = delete;
    smart_ptr& operator=(const smart_ptr& rhs) = delete;

    smart_ptr(smart_ptr&& rhs) : m_ptr(exchange(rhs.m_ptr, nullptr)){}

    smart_ptr& operator=(smart_ptr&& rhs){        
        if (&rhs == this) return *this;
        delete m_ptr;
        m_ptr = exchange(rhs.m_ptr,nullptr);
        return *this;
    }
    T& operator*() const { return *m_ptr; }
    T* operator->() const { return m_ptr; }
};

void func()
{
    auto ptr = smart_ptr<resource>(1, 2); // now ptr guarantee the release of resource
    // ...
}

请注意,无论 ptr 声明后发生什么, ptr 函数终止时都将被销毁(无论它如何终止)。

由于 是 ptr 本地对象,因此在函数堆栈帧倒带时将调用析构函数。因此,我们确信 resource 将得到妥善清理。

使用案例

标准库中C++示例包括 std::unique_ptr 、 std::ofstream 、 std::lock_guard 等。

使用 RAII,可以轻松管理资源,如 new / 、/ 、获取/释放、互斥锁/解锁、文件打开/关闭、计数 ++ /、 malloc 数据库连接/ free delete -- 断开连接或供应有限的任何其他资源。

智能指针

接下来笔者将介绍RAII在C++中最强的应用:智能指针。

C++中一个非常常见的应用场景就是调用一个函数来产生一个对象,通过std::make_xxx或者new一个对象,那么就拥有了这个对象的所有权,然后消费这个对象,最后手动释放指针。如以下代码所示。

class my_struct
{
public:
    my_struct() = default;
};

template <typename T>
T* get_object()
{
    return new T();
}

int main()
{
    auto obj = get_object<my_struct>();
    // consume the object
    // ...
    // consume finish
    delete obj;
}

在大型应用程序中,指针的产生和消费错综复杂,写到后面程序员根本不记得自己有没有释放指针,或者某处地方读取一个已经释放的指针直接导致segmentation fault程序崩溃。而这就是C/C++各种内存泄漏的万恶之源。

而自从C++11推出智能指针后,其极大地减轻了C++开发者们内存管理的压力。通过在裸指针上包一层智能指针,再也不用通过手动 delete来释放内存了。

除了 STL 的智能指针,C++ 准标准库 Boost 的智能指针,比如 boost::scoped_ptrboost::shared_arrayboost::intrusive_ptr也可在实践中使用

返回类型 合法性 说明
std::shared_ptr<T> 安全拷贝引用计数智能指针,推荐
std::unique_ptr<T> 通过移动语义返回独占智能指针,推荐
std::shared_ptr<T>& 引用局部变量,生命周期已结束,危险
T& / T* 同上,返回局部变量引用或地址,危险
unique_ptr

unique_ptr是一种独占式的智能指针,即同一时间只能有一个unique_ptr指向一个对象,不能进行复制和赋值操作。当unique_ptr超出作用域时,它所指向的对象会被自动释放。

  • unique_ptr:
    • get():返回指向所管理对象的裸指针。
    • release():释放unique_ptr对所管理对象的所有权,返回指向该对象的裸指针。
    • reset():释放unique_ptr对所管理对象的所有权,并接管新的对象。
    • operator*():返回所管理对象的引用。
    • operator->():返回所管理对象的指针。

unique_ptr分为以下两种:

  • 指向单个对象
    std::unique_ptr<Type> p1; // p1关联Type对象
  • 指向一个数组
    unique_ptr<Type[]> p2; // p2关联Type对象数组

只允许基础指针的一个所有者。 除非你确信需要 shared_ptr,否则请将该指针用作 POCO 的默认选项。

可以移到新所有者,但不能复制或共享,替换已弃用的 auto_ptr。与 boost::scoped_ptr相比, unique_ptr小巧高效;大小等同于一个指针且支持 rvalue 引用,从而可实现快速插入和对C++ 标准库集合的检索。

头文件:<memory>

下面的代码将展示如何用 std::unique_ptr来管理指针。

class my_struct
{
public:
    my_struct() = default;
};

template <typename T>
std::unique_ptr<T> get_object()
{
    return std::unique_ptr<T>(new T());
}

int main()
{
    auto obj = get_object<my_struct>();
    // consume the object
    // ...
    // consume finish
}

智能指针的方便之处在于它会在自己的析构函数中执行 delete操作而不需程序员手动释放。在上述代码中,当main函数退出时, std::unique_ptr在自己的析构函数中释放指针,而为了防止有别的 std::unique_ptr指向自己管理的对象而导致的提早释放与空指针访问, std::unique_ptr禁止了 copy constructorcopy assignment。有人可能会疑惑,为什么 get_object函数创建的 unique_ptr为什么没有在函数返回前释放指针?这是因为 std::unique_ptr实现了 move constructor(一种可以将资源从另一个对象“偷”过来的构造函数)并在返回时将指针传给了main函数中 obj变量。如果不太理解发生了什么,可以看一下以下我自己尝试实现的 unique_ptr.

template <typename T>
class unique_ptr
{
private:
    T* _ptr;
public:
    // Construct from plain pointer
    explicit unique_ptr(T* ptr) : _ptr(ptr) {
        std::cout << "unique_ptr constructed" << std::endl;
    };

    // Move constructor
    unique_ptr(unique_ptr &&ptr) noexcept : _ptr(ptr._ptr) {
        ptr._ptr = nullptr;
        std::cout << "unique_ptr move constructed" << std::endl;
    }

    // Copy constructor is forbidden
    unique_ptr(unique_ptr &ptr) = delete;

    // Move assignment
    unique_ptr& operator=(unique_ptr &&ptr) noexcept {
        if (this == &ptr) {
            return *this;
        }
        _ptr = ptr._ptr;
        ptr._ptr = nullptr;
        return *this;
    }

    // Copy assignment is forbidden
    unique_ptr& operator=(unique_ptr &ptr) = delete;

    ~unique_ptr() {
        delete _ptr;
        std::cout << "unique_ptr destructed" << std::endl;
    }

    T* operator->() {
        return _ptr; 
    }
};

代码看上去比较复杂,不过我将一个方法一个方法地和大家分析。

  1. 第8行代码实现了最基本的构造函数:从一个裸指针开始构造。
  2. 第13行实现了 move constructor,这个方法会用一个已有的 unique_ptr来构造一个新的对象,它将旧 unique_ptr的指针替换为 nullptr来防止多个指针指向相同对象。
  3. 第19行禁止了 copy constructor的使用,因为不允许多个指针指向同一对象。
  4. 第22行实现了 move assignment,原理与 move constructor相同。
  5. 第32行禁止了 copy assignment,原理与 copy constructor相同。
  6. 第34行是析构函数,将最终释放指针。
  7. 第39行实现了 operatoroverload,使得我们可以像访问普通指针一样访问 unique_ptr

我们来用我们自己定义的 unique_ptr运行一下看会发生什么:

class my_struct
{
public:
    std::string _name = "name";

    my_struct() = default;
    explicit my_struct(std::string name) : _name(std::move(name)) {
        std::cout << "my_struct constructed" << std::endl;
    }
    ~my_struct() {
        std::cout << "my_struct destructed" << std::endl;
    }
};

template<typename T>
unique_ptr<T>get_object()
{
    return unique_ptr<T>(new T("struct name"));
}

int main() {
    unique_ptr<my_struct> obj = get_object<my_struct>();
    std::cout << obj->_name << std::endl;
}

console output:

my_struct constructed
unique_ptr constructed
struct name
my_struct destructed
unique_ptr destructed

首先, my_struct被构造,然后 unique_ptr被构造,并且可以发现, my_struct的析构函数会在 unique_ptr的析构函数返回前执行,这意味着我们成功地将指针的life cycle绑定到了 unique_ptr上!不过,细心的同学可能发现了,全程 unique_ptrmove constructor都没有被call过,但是我之前明确说了,main函数中的 obj是用 get_object函数中构造的 unique_ptr通过 move constructor构造的。可是为什么我们没有抓到 move constructor打印出来的东西呢?这是因为C++编译器做了一个叫做 copy elision的优化,来避免不必要的构造和析构,例如本例中,两个函数中的 unique_ptr对象其实是一个东西,因此他们之间的转换和赋值被优化掉了。如果我们通过 std::move来强制 move constructor发生,如下所示:

我们将看到这样的信息:

my_struct constructed
unique_ptr constructed
unique_ptr move constructed
unique_ptr destructed
struct name
my_struct destructed
unique_ptr destructed

此时我们可以清晰地看到,main函数中的 obj是通过 move constructor构造的,并且在其构造完成之后, get_object函数中构造的 unique_ptr对象被析构了,因为我们已经提早将其内部指针替换成了 nullptr, 其析构函数什么都不会释放。

只有unique_ptr支持以ptr[i]这种形式访问指向的数组。这是因为unique_ptr是一种独占式的智能指针,它所管理的对象在内存中是连续存储的,因此可以像访问数组一样使用下标运算符[]来访问其中的元素。shared_ptr和weak_ptr都不支持以ptr[i]这种形式访问指向的数组,因为它们所管理的对象可能不是连续存储的,因此不能像访问数组一样使用下标运算符[]来访问其中的元素。

shared_ptr

shared_ptr是一种共享式的智能指针,可以有多个shared_ptr指向同一个对象,它们共享对象的所有权。当最后一个shared_ptr超出作用域时,它所指向的对象会被自动释放。

  • shared_ptr:
    • get():返回指向所管理对象的裸指针。
    • use_count():返回当前有多少个shared_ptr共享所管理对象的所有权。
    • reset():释放shared_ptr对所管理对象的所有权,并接管新的对象。
    • operator*():返回所管理对象的引用。
    • operator->():返回所管理对象的指针。

采用引用计数的智能指针。 如果你想要将一个原始指针分配给多个所有者(例如,从容器返回了指针副本又想保留原始指针时),请使用该指针。 直至所有 shared_ptr 所有者超出了范围或放弃所有权,才会删除原始指针。 大小为两个指针;一个用于对象,另一个用于包含引用计数的共享控制块。

头文件:<memory>

它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。出了可以通过new来构造,还可以通过传入auto_ptr,unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

class Test
{
public:
    Test(string s)
    {
        str = s;
       cout<<"Test creat\n";
    }
    ~Test()
    {
        cout<<"Test delete:"<<str<<endl;
    }
    string& getStr()
    {
        return str;
    }
    void setStr(string s)
    {
        str = s;
    }
    void print()
    {
        cout<<str<<endl;
    }
private:
    string str;
};

int main()
{
    shared_ptr<Test> ptest(new Test("123"));
    shared_ptr<Test> ptest2(new Test("456"));
    cout<<ptest2->getStr()<<endl;
    cout<<ptest2.use_count()<<endl;
    ptest = ptest2;//"456"引用次数加1,“123”销毁
    ptest->print();
    cout<<ptest2.use_count()<<endl;//2
    cout<<ptest.use_count()<<endl;//2
    ptest.reset();
    ptest2.reset();//此时“456”销毁
    cout<<"done !\n";
    return 0;
}
weak_ptr

weak_ptr是一种弱引用的智能指针,它可以用于解决shared_ptr循环引用的问题。weak_ptr不会增加对象的引用计数,因此不能直接访问对象,需要通过lock()方法获取一个shared_ptr来访问对象。

结合 shared_ptr 使用的特例智能指针。 weak_ptr 提供对一个或多个 shared_ptr 实例拥有的对象的访问,但不参与引用计数。 如果你想要观察某个对象但不需要其保持活动状态,请使用该实例。 在某些情况下,需要断开 shared_ptr 实例间的循环引用。

头文件:<memory>

  • weak_ptr:
    • expired():判断所管理对象是否已经被释放。
    • lock():返回一个shared_ptr,指向所管理对象,如果所管理对象已经被释放,则返回一个空的shared_ptr。

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
    shared_ptr<B> pb_;
    ~A()
    {
        cout<<"A delete\n";
    }
};
class B
{
public:
    shared_ptr<A> pa_;
    ~B()
    {
        cout<<"B delete\n";
    }
};
 
void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}
 
int main()
{
    fun();
    return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr<B> pb_; 改为weak_ptr<B> pb_; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr<B> p = pa->pb_.lock(); p->print();

总结

(1)如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。这样的情况包括:

  • 将指针作为参数或者函数的返回值进行传递的话,应该使用 shared_ptr;
  • 两个对象都包含指向第三个对象的指针,此时应该使用 shared_ptr 来管理第三个对象;
  • STL 容器包含指针。很多 STL 算法都支持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出 warning)和 auto_ptr(行为不确定)。如果你的编译器没有提供 shared_ptr,可使用 Boost 库提供的 shared_ptr。

(2)如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。如果函数使用 new 分配内存,并返还指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。这样,所有权转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。可将 unique_ptr 存储到 STL 容器中,只要对容器元素不使用拷贝操作的算法即可(如 sort())。例如,可在程序中使用类似于下面的代码段。

unique_ptr<int> make_int(int n) {
    return unique_ptr<int>(new int(n));
}

void show(unique_ptr<int>& p1) {
    cout << *p1 << ' ';
}

int main() {
//...
    vector<unique_ptr<int>> vp(size);
    for(int i = 0; i < vp.size(); i++) {
		vp[i] = make_int(rand() % 1000);// copy temporary unique_ptr}
    vp.push_back(make_int(rand() % 1000));// ok because arg is temporaryfor_each(vp.begin(), vp.end(), show);// use for_each()//...}

其中 push_back 调用没有问题,因为它返回一个临时 unique_ptr,该 unique_ptr 被赋给 vp 中的一个 unique_ptr。另外,如果按值而不是按引用给 show() 传递对象,for_each() 将非法,因为这将导致使用一个来自 vp 的非临时 unique_ptr 初始化 pi,而这是不允许的。前面说过,编译器将发现错误使用 unique_ptr 的企图。

在 unique_ptr 为右值时,可将其赋给 shared_ptr,这与将一个 unique_ptr 赋给另一个 unique_ptr 需要满足的条件相同,即 unique_ptr 必须是一个临时对象。与前面一样,在下面的代码中,make_int() 的返回类型为 unique_ptr<int>:

unique_ptr<int> pup(make_int(rand() % 1000));// ok
shared_ptr<int> spp(pup);// not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000));// ok

模板 shared_ptr 包含一个显式构造函数,可用于将右值 unique_ptr 转换为 shared_ptr。shared_ptr 将接管原来归 unique_ptr 所有的对象。

在满足 unique_ptr 要求的条件时,也可使用 auto_ptr,但 unique_ptr 是更好的选择。如果你的编译器没有unique_ptr,可考虑使用 Boost 库提供的 scoped_ptr,它与 unique_ptr 类似。

(3)虽然说在满足 unique_ptr 要求的条件时,使用 auto_ptr 也可以完成对内存资源的管理,但是因为 auto_ ptr 不够安全,不提倡使用,即任何情况下都不应该使用 auto_ptr。

(4)为了解决 shared_ptr 的循环引用问题,我们可以祭出 weak_ptr。

(5)在局部作用域(例如函数内部或类内部),且不需要将指针作为参数或返回值进行传递的情况下,如果对性能要求严格,使用 scoped_ptr 的开销较 shared_ptr 会小一些。

Return Type Resolver

意向:推断要初始化或分配给的对象的类型。

实现:使用模板化转换运算符。

又称:返回类型重载。

problem:

int from_string(const char *str) { return std::stoi(str); }
float from_string(const char *str) { return std::stof(str); } // error

solution:

class from_string
{
    const string m_str;
public:
    from_string(const char *str) : m_str(str) {}
    template <typename type>
    operator type(){
        if constexpr(is_same_v<type, float>)        
						return stof(m_str);
        else if (is_same_v<type, int>)              
						return stoi(m_str);
    }
};

int n_int = from_string("123");
float n_float = from_string("123.111");
// Will only work with C++17 due to `is_same_v`

使用案例

当您使用 nullptr (在 C++11 中介绍)时,这是一种在后台运行以推断正确类型的技术,具体取决于它所分配到的指针变量。

您还可以基于返回类型克服函数重载限制,如上所述。

返回类型解析程序还可用于提供赋值的泛型接口,独立于分配给的对象。

Type Erasure

意向:创建可处理各种具体类型的通用容器。

实现:可以通过、模板、多态、联合、代理类等实现 void* 。

又称:鸭子打字。

C++ 是一种具有强类型的静态类型语言。在静态类型语言中,对象类型是已知的,并在编译时设置。在动态类型语言中,类型与运行时值相关联。

在强类型语言中,对象的类型在编译后不会更改。

为了克服这种限制并提供动态类型语言等功能,库设计人员提出了各种通用容器类型,如 std::any (C++17), std::variant (C++17), std::function (C++11)等。

不同类型的擦除技术

关于如何实施这个成语,没有一个严格的规则;它可以有各种形式,有其自身的缺点,如下所示:

使用 void* 键入擦除(如 C 语言)

void qsort (void* base, size_t num, size_t size,
            int (*compare)(const void*,const void*));

缺点:不安全,每种类型都需要单独的比较功能。

使用模板键入擦除

template <class RandomAccessIterator>
  void sort(RandomAccessIterator first, RandomAccessIterator last);

缺点:可能导致函数模板实例化较多,编译时间较长

使用多态性进行类型擦除

struct base { virtual void method() = 0; };
struct derived_1 : base { void method() { cout << "derived_1\n"; } };
struct derived_2 : base { void method() { cout << "derived_2\n"; } };

// We don't see a concrete type (it's erased) though can dynamic_cast
void call(base* ptr) { ptr->method(); };

缺点:运行时成本(动态调度、间接寻址、vtable 等)。

使用联合进行类型擦除

struct Data {};
union U {
    Data d;         // occupies 1 byte
    std::int32_t n; // occupies 4 bytes
    char c;         // occupies 1 byte
    ~U() {}         // need to know currently active type
}; // an instance of U in total occupies 4 bytes.

缺点:不是类型安全的。

正如我前面提到的,标准库已经有这样的通用容器。

为了更好地理解类型擦除,让我们实现一个这样的 std::any :

struct any 
{
    struct base {};

    template<typename T>
    struct inner: base{
        inner(T t): m_t{std::move(t)} {}
        T m_t;
        static void type() {}
    };

    any(): m_ptr{nullptr}, typePtr{nullptr} {}
    template<typename T>
    any(T && t): m_ptr{std::make_unique<inner<T>>(t)}, typePtr{&inner<T>::type} {}
    template<typename T>
    any& operator=(T&& t){
        m_ptr = std::make_unique<inner<T>>(t); 
        typePtr = &inner<T>::type;
        return *this;
    }
private:
    template<typename T>
    friend T& any_cast(const any& var);

    std::unique_ptr<base> m_ptr = nullptr;
    void (*typePtr)() = nullptr;
};

template<typename T>
T& any_cast(const any& var)
{
    if(var.typePtr == any::inner<T>::type)
        return static_cast<any::inner<T>*>(var.m_ptr.get())->m_t;
    throw std::logic_error{"Bad cast!"};
}

int main()
{
    any var(10);
    std::cout << any_cast<int>(var) << std::endl;
    var = std::string{"some text"};
    std::cout << any_cast<std::string>(var) << std::endl;
    return 0;
}

特别是,这里要注意的是我们如何利用空静态方法,即 inner<T>::type() 确定 any_cast<T> 中的模板实例类型。

使用案例

用于处理来自函数/方法的多种类型的返回值(尽管这不是推荐的建议)。

CRTP

意向:实现静态多态性

实现:利用基类模板专用化。

又称:颠倒继承、静态多态性

problem:

struct obj_type_1
{
    bool operator<(const value &rhs) const {return m_x < rhs.m_x;}
    // bool operator==(const value &rhs) const;
    // bool operator!=(const value &rhs) const;    
    // List goes on. . . . . . . . . . . . . . . . . . . .
private:
    // data members to compare
};

struct obj_type_2
{
    bool operator<(const value &rhs) const {return m_x < rhs.m_x;}
    // bool operator==(const value &rhs) const;
    // bool operator!=(const value &rhs) const;    
    // List goes on. . . . . . . . . . . . . . . . . . . .
private:
    // data members to compare
};

struct obj_type_3 { ...
struct obj_type_4 { ...
// List goes on. . . . . . . . . . . . . . . . . . . .

对于每个可比较对象,您需要定义相应的比较运算符。这是多余的,因为如果我们有一个 operator < ,我们可以在此基础上重载其他运算符。

因此,是唯一一个具有类型信息的运算符, operator < 其他运算符可以出于可重用性目的而独立于类型。

solution:

奇怪的重复模板模式实现规则很简单,分离出类型依赖和独立的功能,并使用模板专用化将类型无关的功能与基类绑定。

上面的行乍一看似乎很神秘。因此,请考虑上述问题的以下解决方案,以便更清楚:

template<class derived>
struct compare {};
struct value : public compare<value> 
{
    value(const int x): m_x(x) {}
    bool operator<(const value &rhs) const { return m_x < rhs.m_x; }
private:
    int m_x;
};

template <class derived>
bool operator>(const compare<derived> &lhs, const compare<derived> &rhs) {
    // static_assert(std::is_base_of_v<compare<derived>, derived>); // Compile time safety measures
    return (static_cast<const derived&>(rhs) < static_cast<const derived&>(lhs));
}

/*  Same goes with other operators
 == :: returns !(lhs < rhs) and !(rhs < lhs)
 != :: returns !(lhs == rhs)
 >= :: returns (rhs < lhs) or (rhs == lhs)
 <= :: returns (lhs < rhs) or (rhs == lhs) 
*/

int main()
{   
    value v1{5}, v2{10};
    cout <<boolalpha<< "v1 == v2: " << (v1 > v2) << '\n';
    return 0;
}
// Now no need to write comparator operators for all the classes, 
// Write only type dependent `operator <` &  use CRTP

使用案例

CRTP广泛用于静态多态性,无需承担虚拟调度机制的成本。考虑下面的代码,我们没有使用虚拟关键字,仍然实现了多态性(特别是静态多态性)的功能

template<typename specific_animal>
struct animal {
    void who() { implementation().who(); }
private:
    specific_animal& implementation() {return *static_cast<specific_animal*>(this);}
};

struct dog : public animal<dog> {
    void who() { cout << "dog" << endl; }
};

struct cat : public animal<cat> {
    void who() { cout << "cat" << endl; }
};

template<typename specific_animal>
void who_am_i(animal<specific_animal> & animal) {
    animal.who();
}

CRTP 也可用于优化。正如我们在上面看到的,它还支持代码可重用性。

Virtual Constructor

意向:在不知道其具体类型的情况下创建副本或新对象。

实现:利用多态赋值的重载方法。

又称:工厂方法/设计模式。

problem:

C++支持使用其基类的虚拟析构函数进行多态对象销毁。缺少对创建和复制对象的等效支持,因为 С++ 不支持虚拟构造函数、复制构造函数。

此外,除非您知道对象的静态类型,否则无法创建对象,因为编译器必须知道需要分配的空间量。出于同样的原因,对象的副本也要求其类型在编译时已知。

struct animal {
    virtual ~animal(){ cout<<"~animal\n"; }
};

struct dog : animal {
    ~dog(){ cout<<"~dog\n"; }
};

struct cat : animal {
    ~cat(){ cout<<"~cat\n"; }
};

void who_am_i(animal *who) { // not sure whether dog would be passed here or cat
    // How to `create` the object of same type i.e. pointed by who ?
    // How to `copy` object of same type i.e. pointed by who ?
    delete who; // you can delete object pointed by who
}

solution:

虚拟构造函数技术允许多态创建和复制对象,方法是通过使用虚拟方法将创建行为和对象复制到派生类,从而在C++中创建和复制对象。

以下代码不仅实现了虚拟构造函数(即 create() ),而且还实现了虚拟复制构造函数(即 clone() )。

struct animal {
    virtual ~animal() = default;
    virtual std::unique_ptr<animal> create() = 0;
    virtual std::unique_ptr<animal> clone() = 0;
};

struct dog : animal {
    std::unique_ptr<animal> create() { return std::make_unique<dog>(); }
    std::unique_ptr<animal> clone() { return std::make_unique<dog>(*this); }
};

struct cat : animal {
    std::unique_ptr<animal> create() { return std::make_unique<cat>(); }
    std::unique_ptr<animal> clone() { return std::make_unique<cat>(*this); }
};

void who_am_i(animal *who) {
    auto new_who = who->create();// `create` the object of same type i.e. pointed by who ?
    auto duplicate_who = who->clone(); // `copy` object of same type i.e. pointed by who ?    
    delete who; // you can delete object pointed by who
}

使用案例

提供一个通用接口来仅使用一个类来生成/复制各种类。

SFINAE 和 std::enable_if

意向:从一组重载函数中筛选出不产生有效模板实例化的函数。

实现:由编译器自动实现或使用 std::enable_if .

赋予动机

替换失败不是错误是C++编译器用于在重载解析期间过滤掉某些模板化函数重载的语言功能(不是习语)。

在函数模板的重载解析期间,当将显式指定或推导的类型替换为模板参数失败时,将从重载集中丢弃专用化,而不是导致编译错误。

当类型或表达式格式不正确时,会发生替换失败。

template<class T>
void func(T* t){ // Single overload set
    if constexpr(std::is_class_v<T>){ cout << "T is user-defined type\n"; }
    else { cout << "T is primitive type\n"; }
}

int primitive_t = 6;

struct {char var = '4';} class_t;

func(&class_t);
func(&primitive_t);

我们如何创建具有相同签名的函数的两个集合(分别基于基元类型和用户定义类型)?

solution:

template<class T, typename = std::enable_if_t<std::is_class_v<T>>>
void func(T* t){
    cout << "T is user-defined type\n";
}

template<class T, std::enable_if_t<std::is_integral_v<T>, T> = 0>
void func(T* t){ // NOTE: function signature is NOT-MODIFIED
    cout << "T is primitive type\n";
}

上面的代码片段是使用 std::enable_if 利用 SFINAE 的简短示例,其中第一个模板实例化将等效于 void func<(anonymous), void>((anonymous) * t) 第二个模板 void func(int * t) 实例化。

您可以在here阅读更多 std::enable_if 内容。

使用案例

与 std::enable_if 一起,SFINAE 被大量用于模板元编程。

标准库还在大多数type_traits实用程序中利用了 SFINAE。请考虑以下事项:

// Stolen & trimmed from https://stackoverflow.com/questions/982808/c-sfinae-examples.
template<typename T>
class is_class_type {
    template<typename C> static char test(int C::*);    
    template<typename C> static double test(...);
public:
    enum { value = sizeof(is_class_type<T>::test<T>(0)) == sizeof(char) };
};

struct class_t{};

int main()
{
    cout<<is_class_type<class_t>::value<<endl;    // 1
    cout<<is_class_type<int>::value<<endl;        // 0
    return 0;
}

如果没有SFINAE,你会得到一个编译器错误,比如“ 0 不能转换为非类类型的 int 成员指针”,因为两者的 test 重载仅在返回类型方面有所不同。

因为不是类,所以 int 它不能有类型的 int int::* 成员指针。

Proxy

意向:使用中间件类实现直观的功能。

实现:通过使用临时/代理类。

也称为: operator [] (即下标)代理,双/两次运算符重载。

赋予动机

大多数人认为这只是关于下标运算符(即 operator[ ] ),但我相信交换数据之间的类型是代理。

我们已经在上面的类型擦除(即.class)中间接看到了这个成语的一个很好的例子 any::inner<> 。但是,我认为再举一个例子将增加我们理解的具体性。

operator [ ] solution

template <typename T = int>
struct arr2D{
private:
    struct proxy_class{
        proxy_class(T *arr) : m_arr_ptr(arr) {}
        T &operator[](uint32_t idx) { return m_arr_ptr[idx]; }
    private:
        T *m_arr_ptr;
    };
    T m_arr[10][10];
public:
    arr2D::proxy_class operator[](uint32_t idx) { return arr2D::proxy_class(m_arr[idx]); }
};

int main()
{
    arr2D<> arr;
    arr[0][0] = 1;
    cout << arr[0][0];
    return 0;
}

使用案例

创建直观的功能,如双运算符重载等 std::any 。

总结

何时实际使用 RAII?

当您有一组步骤来执行任务时,并且两个步骤是理想的(即设置和清理),那么这就是您可以使用RAII的地方。

为什么函数不能按返回类型重载?

不能重载返回类型,因为在函数调用表达式中不强制使用函数的返回值。例如,我只能说get_val();

何时使用返回类型解析程序习惯用法?

当输入类型固定但输出类型可能有所不同时,可以应用返回类型解析程序习惯用法。

什么是C++中的类型擦除?

类型擦除技术用于设计依赖于赋值类型的泛型类型(就像我们在 Python 中所做的那样)。顺便问一下,你知道吗 auto ,或者你现在可以设计一个吗?

应用类型擦除习惯用法的最佳方案?

它在泛型编程中很有用。它还可用于处理来自函数/方法的多种类型的返回值(尽管这不是推荐的建议)。

什么是奇怪的重复模板模式 (CRTP)?

当类 A 具有基类时,会发生 CRTP。该基类是类 A 本身的模板专用化。例如

template <class T>  
class X{...}; 
class A : public X<A> {...};

什么是C++中的代理类?

代理是为另一个类提供修改后的接口的类。

为什么我们在C++中没有虚拟构造函数?

虚拟表(vtable)是为具有一个或多个“虚拟函数”的每个类创建的。每当创建此类对象时,它都会包含一个“虚拟指针”,该指针指向相应 vtable 的基数。每当有虚拟函数调用时,vtable 用于解析为函数地址。

构造函数不能是虚拟的,因为在执行类的构造函数时,内存中没有 vtable。这意味着尚未定义虚拟指针。因此,构造函数应始终是非虚拟的。

虚拟构造函数的用例和需求是什么?

使用基类多态方法创建和复制对象(不知道其具体类型)。

三法则 五法则

浅拷贝(编译器默认生成)

cpp
class Shallow {
public:
    int* data;

    Shallow(int val) {
        data = new int(val);
    }

    // 没有手动写拷贝构造函数 = 编译器默认行为(浅拷贝)
    ~Shallow() {
        delete data;
    }
};
Shallow a(10);
Shallow b = a;  // 浅拷贝,b.data == a.data 指向同一内存
  • abdata 都指向同一地址
  • 两者中任一析构,另一个继续访问 data 就会 UAF 或 double free

深拷贝(用户自定义拷贝构造)

class Deep {
public:
    int* data;

    Deep(int val) {
        data = new int(val);
    }

    // ✅ 自定义深拷贝构造函数
    Deep(const Deep& other) {
        data = new int(*(other.data));
    }

    ~Deep() {
        delete data;
    }

    // ✅ 可选:赋值运算符重载
    Deep& operator=(const Deep& other) {
        if (this != &other) {
            delete data;
            data = new int(*(other.data));
        }
        return *this;
    }
};
Deep a(10);
Deep b = a; // 深拷贝,b.data 指向独立的堆内存
  • abdata 各自独立
  • 一方析构不会影响另一方的内存访问

三法则/五法则

如果你的类中涉及资源管理(如 new/malloc)或定义了以下任何一个,必须显式定义以下函数,否则资源释放可能出错:

  • 拷贝构造函数 T(const T&)
  • 拷贝赋值运算符 operator=
  • 析构函数 ~T()
  • (C++11+)移动构造函数 T(T&&)
  • (C++11+)移动赋值运算符 operator=(T&&)

如果其中一个函数在程序员未首先声明的情况下使用,编译器将以以下默认语义隐式实现它:

  • 析构函数 - 调用对象的所有类类型成员的析构函数
  • 拷贝构造函数 - 从拷贝构造函数的参数的相应成员构造对象的所有成员,调用对象的类类型成员的拷贝构造函数,并执行所有非类类型(例如int或指针)数据成员的普通赋值
  • 拷贝赋值运算符 - 从赋值运算符的参数的相应成员分配对象的所有成员,调用对象的类类型成员的拷贝赋值运算符,并执行所有非类类型(例如int或指针)数据成员的普通赋值。

三法则声称,如果这三个函数中的一个必须由程序员定义,这意味着编译器生成的版本不适合类在一个情况下的需求,而在其他情况下也可能不适合。该术语“三法则”由Marshall Cline于1991年创造。

对此规则的修正是,如果该类的设计以使资源获取初始化(RAII)用于其所有(非微不足道的)成员,则可以留空析构函数(也称为“大二法则”)。这种方法的即用性示例是使用智能指针而不是普通指针。

由于隐式生成的构造函数和赋值运算符只是复制所有类数据成员(“浅拷贝”),因此对于封装复杂数据结构或具有外部引用(例如指针)的类,应定义显式的拷贝构造函数和拷贝赋值运算符,如果需要复制类成员所指向的对象。如果默认行为(“浅拷贝”)实际上是预期的行为,则显式定义虽然冗余,但将是“自我记录代码”,表明这是一个意图而不是一个疏忽。现代C++包括一个语法,可以明确指定需要默认函数,而不必键入函数体。

随着C++11的出现,三法则可以扩展为“五法则”(也称为“大五法则”),因为C++11实现了移动语义,允许目标对象从临时对象中“抓取”(或“窃取”)数据。下面的示例还显示了新的移动成员:移动构造函数和移动赋值运算符。因此,对于“五法则”,我们有以下“特殊成员”:

  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符

存在这样的情况,即类可能需要析构函数,但不能明智地实现拷贝和移动构造函数以及拷贝和移动赋值运算符。例如,当基类不支持这些后续“四大”成员,但派生类的构造函数为其自己使用分配内存时,就会发生这种情况。在C++11中,可以通过明确指定这五个成员为默认值来简化此过程。

模版元编程

模板元编程(英语:Template metaprogramming,缩写:TMP)是一种元编程技术,编译器使用模板产生暂时性的源码,然后再和剩下的源码混合并编译。 这些模板的输出包括编译时期常量、数据结构以及完整的函数。 如此利用模板可以被想成编译期的执行。

模板元编程的核心思想是使用模板来生成代码,而不是在运行时执行代码。通过模板元编程,可以在编译时进行类型检查和优化,从而提高程序的性能和可靠性。此外,模板元编程还可以实现一些高级的编程技巧,例如递归和条件编译。

在C++中,模板元编程通常使用模板元函数模板类来实现。模板元函数是一种特殊的函数模板,它的参数和返回值都必须是常量表达式。模板类也是一种特殊的类模板,它的成员函数和成员变量都必须是常量表达式。在模板元编程中,常量表达式是指在编译时可以计算出结果的表达式。

以下是一个简单的示例,演示了如何使用模板元函数来计算斐波那契数列的第n个数:

template <int N>
struct Fib {
    static const int value = Fib<N-1>::value + Fib<N-2>::value;
};

template <>
struct Fib<0> {
    static const int value = 0;
};

template <>
struct Fib<1> {
    static const int value = 1;
};

int main() {
    std::cout << Fib<10>::value << std::endl; // 输出:55
    return 0;
}

在这个例子中,Fib<N>是一个模板类,它的静态成员变量value表示斐波那契数列的第N个数。当N大于1时,Fib<N>value成员变量由Fib<N-1>Fib<N-2>value成员变量相加得到。当N等于0或1时,Fib<N>value成员变量分别为0和1。通过使用模板元编程,可以在编译时计算斐波那契数列的第N个数,而不是在运行时执行递归函数。

模板元编程的应用非常广泛,以下是一些常见的应用:

  • 代码生成:模板元编程可以用于生成代码,例如序列化和反序列化代码、类型转换代码、代码优化和代码分析工具等。
  • 元函数:模板元函数可以用于计算常量表达式,例如计算斐波那契数列、计算阶乘等。元函数还可以用于类型计算,例如计算类型的大小、计算类型的成员变量和成员函数等。
  • 类型推导:模板元编程可以用于类型推导,例如实现类型转换、类型选择和类型匹配等。
  • 元编程框架:模板元编程可以用于实现元编程框架,例如Boost.MPL和STLSoft等。

模版编程

模板元函数(Template Metafunction)和模板函数(Function Templates)是 C++ 中的两种不同的模板技术。

模板函数是定义一个函数模板,可以用不同的参数类型调用该模板来生成特定类型的函数。例如:

template <typename T>
T add(T a, T b) {
  return a + b;
}

int main() {
  int result = add<int>(3, 4); // result is 7
  double result2 = add<double>(1.5, 2.5); // result2 is 4.0
  return 0;
}

模板元函数是定义一个类型转换模板,可以用不同的类型作为参数调用该模板来生成特定类型的类型。例如:

template <typename T>
struct add_pointer {
  typedef T* type;
};

int main() {
  typedef add_pointer<int>::type IntPointer; // IntPointer is int*
  typedef add_pointer<double>::type DoublePointer; // DoublePointer is double*
  return 0;
}

可以看到,模板元函数返回的是一个类型,而模板函数返回的是一个值。另外,模板元函数通常使用 typedef 或 using 来定义类型别名,而模板函数则直接返回值。

自动推导返回值类型+完美转发的模版函数

template <typename Func, typename... Args>
auto call(Func&& func, Args&&... args)
  -> decltype(std::forward<Func>(func)(std::forward<Args>(args)...))
{
  return std::forward<Func>(func)(std::forward<Args>(args)...);
}

POD

POD(Plain Old Data,普通旧数据)类型是从 C++11 开始引入的概念

  • Plain 代表一个对象是一个普通类型
  • Old 代表一个对象可以与 C 兼容

通俗地讲,一个类、结构、共用体对象或非构造类型对象能通过二进制拷贝(如 memcpy())后还能保持其数据不变正常使用的就是POD类型的对象。

在 C++ 中,POD(普通旧数据)类型是一种既简单又标准布局的类或结构。让我们分解一下每个术语的含义:

  • 普通:普通的类或结构是没有非平凡的默认构造函数、复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符或析构函数的类或结构。
  • 标准布局:标准布局类或结构没有具有不同访问控制级别的数据成员,没有虚函数,没有引用类型的非静态数据成员,没有本身不是标准布局类型的基类,并且数据成员之间没有填充。这意味着类或结构在内存中具有一致的布局,可以使用低级 C 样式函数轻松操作。

当一个类或结构既普通又标准布局时,它被认为是 POD 类型。POD 类型很有用,因为可以使用低级 C 风格函数(如 memcpy())安全地操作它们,并且可以在非 POD 类型不能使用的某些上下文中使用它们,例如在联合中。

不同类型的对象意味着对象的成员在内存中的布局是不同的。

在某些情况下,布局是有规范明确的定义,但如果类或结构包含某些 C++ 语言功能,如虚拟基类、 虚函数、 具有不同的访问控制的成员,则不同编译器会有不同的布局实现,具体取决于编译器对代码的优化方式,比如实现内存对齐,减少访存指令周期。

例如,如果类具有虚函数,该类的所有实例都会包含一个指向虚函数表的指针,那么这个对象就不能直接通过二进制拷贝的方式传到其它语言编程的程序中使用。

C++ 给定对象类型取决于其特定的内存布局方式,一个对象是普通、标准布局还是 POD 类型,可以根据标准库函数模板来判断:

is_trivial<T>
is_standard_layout<T>
is_pod<T>

使用时需要包含头文件<type_traits>

POD类型要求

  1. 不能包含任何非静态成员函数:POD类型不能包含任何非静态成员函数,因为非静态成员函数需要依赖于对象才能够被调用,这与POD类型的简单性质相违背。
  2. 不能包含任何虚函数:POD类型不能包含任何虚函数,因为虚函数需要额外的虚函数表来维护,这会增加类型的复杂性。
  3. 不能包含任何虚基类:POD类型不能包含任何虚基类,因为虚基类需要额外的虚基类指针来维护,这会增加类型的复杂性。
  4. 不能包含任何成员对象,其构造函数或析构函数有用户定义的行为:POD类型不能包含任何成员对象,其构造函数或析构函数有用户定义的行为,因为这会增加类型的复杂性。POD类型的构造函数和析构函数都是默认的,也就是说它们不需要进行任何初始化或清理工作。
  5. 不能包含任何成员对象,其构造函数或析构函数中调用了非静态成员函数或虚函数:POD类型不能包含任何成员对象,其构造函数或析构函数中调用了非静态成员函数或虚函数,因为这会增加类型的复杂性。
  6. 不能包含任何成员对象,其构造函数或析构函数中有try-catch块:POD类型不能包含任何成员对象,其构造函数或析构函数中有try-catch块,因为这会增加类型的复杂性。
  7. 不能包含任何成员对象,其构造函数或析构函数中有函数try块:POD类型不能包含任何成员对象,其构造函数或析构函数中有函数try块,因为这会增加类型的复杂性。
  8. 不能包含任何成员对象,其类型本身不是POD类型:POD类型不能包含任何成员对象,其类型本身不是POD类型,因为这会增加类型的复杂性。

POD类型性质

  1. POD类型是一个标量类型、一个POD结构体或一个数组类型。
  2. POD类型的构造函数和析构函数都是默认的,也就是说它们不需要进行任何初始化或清理工作。
  3. POD类型的所有非静态成员都是公有的。
  4. POD类型的所有非静态成员都是其他POD类型或标量类型。
  5. POD类型可以使用memcpy()函数进行内存拷贝,也可以使用memset()函数进行内存清零。
  6. POD类型可以使用C语言的结构体初始化语法进行初始化。

POD类型常见的使用场景:

  1. 内存操作:POD类型可以使用memcpy()函数进行内存拷贝,也可以使用memset()函数进行内存清零。这些操作在一些底层编程中非常常见,例如网络编程、操作系统编程等。
  2. 结构体初始化:POD类型可以使用C语言的结构体初始化语法进行初始化,这使得代码更加简洁、清晰。在一些需要大量结构体初始化的场景下,使用POD类型可以提高代码的可读性和可维护性。
  3. 数据序列化:在进行数据序列化时,POD类型可以更加方便地进行二进制数据的读写。例如,在进行网络数据传输时,可以使用POD类型将数据进行打包和解包。

普通类型

当类或结构体同时满足如下几个条件时是普通类型:

  1. 没有虚函数或虚基类;
  2. 由C++编译器提供默认的特殊成员函数(默认的构造函数、拷贝构造函数、移动构造函数、赋值运算符、移动赋值运算符和析构函数);
  3. 数据成员同样需要满足条件(1)和(2)。

注意,普通类型可以具有不同的访问说明符(private等)。下面我们使用模版类std::is_trivial<T>::value来判断数据类型是否为普通类型。

#include <iostream>
#include <string>
class A { A() {} };
class B { B(B&) {} };
class C { C(C&&) {} };
class D { D operator=(D&) {} };
class E { E operator=(E&&) {} };
class F { ~F() {} };
class G { virtual void foo() = 0; };
class H : virtual F{};
class I {};

int main()
{
	std::cout << std::is_trivial<A>::value;  //有自定义构造函数
	std::cout << std::is_trivial<B>::value;  //有自定义的拷贝构造函数
	std::cout << std::is_trivial<C>::value;  //有自定义的移动构造运算符
	std::cout << std::is_trivial<D>::value;  //有自定义的赋值运算符
	std::cout << std::is_trivial<E>::value;  //有自定义的移动赋值运算符
	std::cout << std::is_trivial<F>::value;  //有自定义的析构函数
	std::cout << std::is_trivial<G>::value;  //有虚函数
	std::cout << std::is_trivial<H>::value;  //有虚基类
	std::cout << std::is_trivial<I>::value;   //普通的类return 0;
}

程序输出结果如下:

000000001

标准布局类型

当类或结构体同时满足如下几个条件时是标准布局类型:

  1. 没有虚函数或虚基类;
  2. 所有非静态数据成员都具有相同的访问说明符;
  3. 在继承体系中最多只有一个类中有非静态数据成员;
  4. 子类中的第一个非静态成员的类型与其基类不同;此规则是因为 C++ 允许优化不包含成员基类而产生的。在 C++ 标准中,如果基类没有任何数据成员,基类应不占用空间,为了体现这一点,C++ 标准允许派生类的第一个成员与基类共享同一地址空间。但是如果派生类的第一个非静态成员的类型和基类相同,由于 C++ 标准要求相同类型的对象的地址必须不同,编译器就会为基类分派一个字节的地址空间。比如下面的代码:
class B1{};
class B2{};
 
class D1: public B1
{
    B1 b;
    int i ;
};
class D2: public B1
{
	  B2 b ;
		int i ;
}

D1 和 D2 类型的对象内存布局应该是相同的,但实际上是不同的,因为 D1 中基类 B1 和对象 b 都占用了 1 个字节,D2 中基类 B1 为空,并不占用内存空间。D1 和 D2 的内容布局从左至右如下图所示:

Image in a image block

注意,这条规定 GNU C++ 遵守,Visual C++ 并不遵守。

5.所有非静态数据成员同样需要满足条件(1)、(2)、(3)和(4)。

考察如下程序:

#include <iostream>
using namespace std;

class A { virtual void foo() = 0; };

class B {
private:
	int a;
public:
	int b;
};

class C1 {
	int x1;
};

class C:C1
{
	int x;
};

class D1 {};
class D : D1
{
	D1 d1;
};

class E : virtual C1 {};
class F { B x; };
class G :C1, D1 {};

int main()
{
	std::cout << std::is_standard_layout<A>::value ;  // 有虚函数  
	std::cout << std::is_standard_layout<B>::value ;  // 成员a和b具有不同的访问权限  
	std::cout << std::is_standard_layout<C>::value ;  // 继承树有非静态数据成员的类超过1个  
	std::cout << std::is_standard_layout<D>::value ;  // 第一个非静态成员是基类类型
	std::cout << std::is_standard_layout<E>::value ;  // 有虚基类
	std::cout << std::is_standard_layout<F>::value ;  // 非静态成员x不符合标准布局类型
	std::cout << std::is_standard_layout<G>::value ;  // return 1
	return 0;
}

程序运行结果:

00000001

POD作用

POD 对象与 C 语言中的对象具有一些共同的特性,包括初始化、复制、内存布局与寻址:

  1. 可以使用字节赋值,比如用 memset、memcpy 对 POD 类型进行赋值操作;
  2. 对 C 内存布局兼容,POD 类型的数据可以使用 C 函数进行操作且总是安全的;
  3. 保证了静态初始化的安全有效,静态初始化可以提高性能,如将 POD 类型对象放入 BSS 段默认初始化为 0。

下面看一下 POD 类型的二进制拷贝示例:

#include <iostream> 
using namespace std;

class A {
public:
  int x;
	double y;
};

int main()
{
	if (std::is_pod<A>::value)
	{
		std::cout << "before" << std::endl;
		A a;
		a.x = 8;
		a.y = 10.5;
		std::cout << a.x << std::endl;
		std::cout << a.y << std::endl;

		size_t size = sizeof(a);
		char *p = new char[size];
		memcpy(p, &a, size);
		A *pA = (A*)p;

		std::cout << "after" << std::endl;
		std::cout << pA->x << std::endl;
		std::cout << pA->y << std::endl;

		delete p;
	}
	return 0;
}

程序运行结果如下:

before
8
10.5
after
8
10.5

可见,POD 类型使用字节拷贝可以正常进行赋值操作。

事实上,如果对象是普通类型,不是标准布局,例如类同时有 public 与 private 的非静态数据成员,也可以使用 memcpy 进行字节拷贝赋值。如果对象是标准布局类型,不是普通类型,例如类有复杂的 move 与 copy 构造函数,也可以使用 C 函数进行操作。

Idiom

  1. RAII(Resource Acquisition Is Initialization):这个成语用于管理内存或文件句柄等资源。 思路是把资源封装在一个类中,用构造函数获取资源,用析构函数释放资源。 这可确保资源始终得到妥善管理,即使遇到异常或其他错误也是如此。
  2. The Pimpl Idiom:这个惯用语用于向类的用户隐藏实现细节。 这个想法是定义一个包含实现细节的私有内部类,并在外部类的公共接口中使用指向该类的指针。 这允许在不影响公共接口的情况下更改实现。
  3. The Curiously Recurring Template Pattern (CRTP):这个惯用语用于通过继承一个以派生类作为模板参数的模板来向类添加功能。 这允许派生类访问基类的功能,并添加自己的功能。
  4. 单例模式:这个习惯用法用于确保只创建一个类的一个实例。 这个想法是定义一个私有构造函数和一个返回指向类的单个实例的指针的静态方法。 这确保类始终以一致的方式使用。
  5. 工厂方法模式:这个习语用于创建对象而不指定将要创建的对象的确切类。 这个想法是定义一个工厂类,它有一个创建对象的方法。 然后可以对工厂类进行子类化以创建不同类型的对象。 这允许更灵活和可扩展的代码。
  6. 模板方法模式:这个习语用于在基类中定义算法的骨架,但允许子类覆盖算法的某些步骤。 这个想法是在基类中定义一个模板方法,该方法调用由子类实现的抽象方法。 这允许更灵活和可扩展的代码。
  7. 适配器模式:这个习惯用法用于将一个类的接口转换成客户期望的另一个接口。 这个想法是定义一个实现预期接口的类,但在内部使用适配类的实例来执行实际工作。 这允许代码重用和与现有代码的兼容性。
  8. 装饰者模式:这个习语用于动态地向对象添加功能。 这个想法是定义一个包装现有对象并向其添加新行为的类。 这允许更灵活和可扩展的代码。
  9. The Command Pattern:这个习惯用法用于将请求封装为一个对象,允许它被视为一流的对象。 这个想法是定义一个代表命令的类,以及执行命令和撤消命令的方法。 这允许更灵活和可扩展的代码。
  10. 观察者模式:这个习语用于定义对象之间的一对多关系,这样当一个对象改变状态时,它的所有依赖者都会被通知并自动更新。 这个想法是定义一个维护观察者列表的主题类,并定义一个由依赖对象实现的观察者接口。 这允许更灵活和可扩展的代码。
  11. The Bridge Pattern:这个成语用于将抽象与其实现分开,允许它们独立变化。 其思想是定义一个代表抽象的抽象基类,并定义一个单独的实现类层次结构。 抽象类包含指向实现对象的指针,并将其操作委托给实现。 这允许更灵活和可扩展的代码。
  12. 迭代器模式:这个习惯用法用于提供一种方法来顺序访问聚合对象的元素,而不暴露其底层表示。 这个想法是定义一个迭代器接口,它提供访问聚合对象元素的方法,并定义具体的迭代器类,为特定类型的聚合对象实现接口。 这允许更灵活和可扩展的代码。
  13. Flyweight Pattern:这个习惯用法用于通过在相似对象之间共享尽可能多的数据来最小化内存使用。 这个想法是定义一个创建和管理享元对象的享元工厂类,并定义对象实现的享元接口。 享元工厂维护一个享元对象池,并尽可能返回现有对象,而不是创建新对象。 这允许更有效地使用内存。
  14. 代理模式:这个习语用于为另一个对象提供代理或占位符,允许间接控制或访问它。 其思想是定义一个与真实对象具有相同接口的代理类,并使用代理来控制对真实对象的访问。 这允许更灵活和可扩展的代码。
  15. 访问者模式:这个习语用于将算法与其操作的对象分开。 这个想法是定义一个访问者接口,为算法可以操作的每种类型的对象提供一个方法,并定义具体的访问者类来实现特定算法的接口。 对象接受访问者,并为其类型调用适当的方法。 这允许更灵活和可扩展的代码。
  16. Composite Pattern:这个成语用于表示对象的部分-整体层次结构,允许将它们视为单个对象或对象组。 这个想法是定义一个组件接口来表示单个对象和对象组,并定义复合类来实现对象组的接口。 这允许更灵活和可扩展的代码。
  17. 责任链模式:这个习语用于允许多个对象处理一个请求,而不知道哪个对象会处理它。 这个想法是定义一个对象链,每个对象都有对链中下一个对象的引用。 发出请求时,链中的第一个对象会尝试处理它,如果不能,则将其传递给链中的下一个对象。 这允许更灵活和可扩展的代码。
  18. 状态模式:这个成语用于允许对象在其内部状态改变时改变它的行为。 这个想法是定义一个状态接口,为对象的每个可能状态提供方法,并定义具体的状态类来实现特定状态的接口。 该对象维护对其当前状态的引用,并将其操作委托给状态对象。 这允许更灵活和可扩展的代码。
  19. Memento 模式:这个习惯用法用于在不违反封装的情况下捕获和恢复对象的内部状态。 这个想法是定义一个包含对象状态快照的备忘录类,并定义一个管理备忘录的看守类。 该对象提供创建和恢复纪念品的方法,而管理员则管理这些纪念品。 这允许更灵活和可扩展的代码。
  20. 模板元编程习惯用法:这个习惯用法用于使用模板执行编译时计算。 这个想法是定义一个执行计算的模板类或函数,并使用模板特化和递归来执行复杂的计算。 这允许更高效和灵活的代码。
  21. 命令查询分离(CQS)原则:这个习语用于将修改对象状态(命令)的方法与仅返回对象信息(查询)的方法分开。 这个想法是定义方法,这些方法要么是命令,要么是查询,但不能同时是两者。 这允许更灵活和可扩展的代码。
  22. 非虚拟接口(NVI)成语:这个成语用于使一个类的公共接口成为非虚拟的,同时仍然允许子类覆盖它的行为。 这个想法是定义一个调用私有虚拟实现方法的公共非虚拟接口。 这允许更灵活和可扩展的代码,同时仍然保持封装的好处。
  23. The Expression Template Idiom:这个习语用于为复杂的数学表达式创建高效的代码。 这个想法是定义一组表示表达式组件的模板类,并使用运算符重载来创建表达式。 这允许更高效的代码,因为表达式是在编译时而不是运行时计算的。
  24. The Named Constructor Idiom:该成语用于提供一种更具可读性和灵活性的方式来创建具有多个构造函数的对象。 这个想法是定义一组静态方法来创建具有特定属性的对象,而不是使用具有不同参数的多个构造函数。 这允许更灵活和可扩展的代码,因为可以添加新的构造函数而无需更改现有的构造函数。
  25. 类型擦除成语:这个成语用于为不同类型的对象创建通用接口,而不要求它们共享一个公共基类。 这个想法是定义一个可以保存任何类型对象的类型擦除类,并定义一组可用于操作对象的虚拟方法。 这允许更灵活和可扩展的代码,因为可以在不更改现有代码的情况下添加新类型。
  26. The Substitution Failure Is Not An Error (SFINAE) 成语:该成语用于允许模板根据模板参数的属性选择适当的特化。 这个想法是利用这样一个事实,即模板特化中的替换失败不是错误,而只是导致该特化被丢弃。 这允许更灵活和可扩展的代码,因为模板可用于处理各种各样的类型。
  27. Scoped Guard 成语:这个成语用于在退出作用域时执行清理操作,而不管作用域是如何退出的。 这个想法是定义一个在其析构函数中执行清理操作的类,并在作用域的开头创建该类的实例。 这可确保始终执行清理操作,即使范围因异常或其他错误而退出也是如此。
  28. The Null Object Idiom:该成语用于为类提供默认实现,而不是使用空指针或抛出异常。 这个想法是定义一个空对象类,它实现与真实类相同的接口,但为每个方法提供默认行为。 这允许更灵活和可扩展的代码,因为可以使用空对象代替真实对象而不会导致错误。
  29. The Double Dispatch Idiom:这个习语用于根据两个对象的类型对它们执行动态调度。 这个想法是在每个对象中定义一个虚方法来引用另一个对象,并定义一组重载方法来调用具有适当类型的虚方法。 这允许更灵活和可扩展的代码,因为可以在不更改现有代码的情况下添加新类型。
  30. The Type Traits Idiom:该成语用于提供有关类型的编译时信息,例如类型是指针还是引用,或者它是否具有某个成员函数。 这个想法是定义一组提供所需信息的模板类,并使用模板特化来处理不同的类型。 这允许更高效和灵活的代码。
  31. The CRTP (Curiously Recurring Template Pattern) Idiom with Mixins:这个成语是 CRTP 成语的扩展,它使用 mixin 来为类添加功能。 思路是定义一个继承自基类的mixin类,定义一组同时继承mixin类和基类的模板类。 这允许更灵活和可扩展的代码,因为可以通过创建新的混合类来添加新功能。
  32. The Type Erasure Idiom with std::function:这个习惯用法是类型擦除习惯用法的扩展,它使用 std::function 为具有不同签名的函数创建通用接口。 这个想法是定义一个类型擦除类,它可以保存可以用特定签名调用的任何类型的对象,并使用 std::function 创建一个类型擦除函数对象。 这允许更灵活和可扩展的代码,因为可以在不更改现有代码的情况下添加新功能。
  33. The CRTP Idiom with Policies:这个成语是 CRTP 成语的扩展,它使用策略来为类添加行为。 这个想法是定义一组提供所需行为的策略类,并使用模板专门化来处理不同的策略。 这允许更灵活和可扩展的代码,因为可以通过创建新的策略类来添加新的行为。
  34. The Named Template Parameters 成语:这个成语用于提供更具可读性和灵活性的模板参数。 这个想法是定义一组可以用来代替标准模板参数的命名模板参数,并为每个参数定义默认值。 这允许更灵活和可扩展的代码,因为可以在不更改现有代码的情况下添加新参数。