C++11之委派构造函数

简介: C++11之委派构造函数

委派构造函数(delegating constructor)

委派构造函数是在C++11标准新引入的一项改进,主要目的是为了减少程序员在构造函数方面花费的工作。通过委派构造函数使之对于构造函数的编写更加简单。

场景

在下面这段代码中,定义了一个Class Info。这个类中有俩个成员变量,三个构造函数,但是三个构造函数都共同调用了InitRest方法。所以我们看到各个构造函数都有或多或少的相似之处。为此C++11引入了委派构造函数。

class Info
{
public:
  Info() :type(1), name('a')
  {
    InitRest();
  }
  Info(int i) :type(i), name('a')
  {
    InitRest();
  }
  Info(char ch) :type(1), name(ch)
  {
    InitRest();
  }
private:
  void InitRest(){ /* 其他初始化 */ }
  int type;
  char name;
};


使用成员初始化方式进行优化

这里其实我们可以通过成员初始化方式,可以使得初始化列表变得简洁化。

class Info
{
public:
  Info()
  {
    InitRest();
  }
  Info(int i) :type(i)
  {
    InitRest();
  }
  Info(char ch) :name(ch)
  {
    InitRest();
  }
private:
  void InitRest() { /* 其他初始化 */}
  int type {1};
  char name {'a'};
};


使用委派构造函数法方式进行优化

首先我们需要了解一下概念:委派构造函数说白了就是将构造的任务分派给一个目标构造函数来完成

委派构造函数:初始化列表中调用“基准版本”的构造函数就是委派构造函数。

目标构造函数:被调用“基准版本”构造函数就是目标构造函数。


需要注意的是委派构造函数不能使用初始化列表 初始化成员变量

// 通过委派构造函数进行优化
class Info
{
public:
  Info()
  {
    InitRest();
  }
  Info(int i) : Info()
  {
    type = i;
  }
  Info(char ch) : Info() // 委派构造函数不能使用初始化列表 初始化成员变量
  {
    name = ch;
  }
private:
  void InitRest() { /* 其他初始化 */}
  int type{ 1 };
  char name{ 'a' };
};


然而委派构造函数也不全是优点,构造函数中委派构造和初始化列表是互斥的,所以变量初始化就只能放在函数体内了。

但是初始化列表的调用总是优先于构造函数,所以这就可能会给自己挖坑

委派构造函数与初始化列表我全都要

为了使用委派构造时还能使用初始化列表,我们可以定义一个private 的目标构造函数,并将初始化列表放在这个private的目标构造函数中,这样其他委派构造函数就可以通过委派这个目标构造来实现构造的功能。

// 委派构造函数 + 初始化列表
class Info
{
public:
  Info() :Info(1, 'a') {}
  Info(int i) : Info(i, 'a') {}
  Info(char ch) : Info(1, ch) {} 
private:
  Info(int i, char ch) :type(i), name(ch) {/*其他初始化信息*/}
  int type{ 1 };
  char name{ 'a' };
};


对上述俩种委派构造进行分析

上述俩种委派构造看似优化了代码,但是却存在着看不到坑。

下面这段测试就会导致预期与实际不否。(注:为了方便测试,将成员变量的作用域设为public)

这段测试看似结果正确,但真就是我们详细的那样吗?

分析:

首先Info(int)会委派Info(),然后调用InitRest方法对type进行加1,此时type=2,然后在执行Info(int)构造函数内部,这时type=5。可见这个5”来之不易“啊。

所以这俩种委派构造在这种场景下都不算一个好的代码。


链状委派构造

在构造函数较多情况下,会形成链状的委派构造关系。

// 链状委派构造函数
class Info
{
public:
  Info() :Info(1) {}
  Info(int i) : Info(i, 'a') {}
  Info(char ch) : Info(1, ch) {} 
private:
  Info(int i, char ch) :type(i), name(ch) {}
public:
  int type{ 1 };
  char name{ 'a' };
};


在上面这段代码种,Info()委派Info(int)进行构造工作,然后Info(int)委派Info(int, char)。这就是链状委派

委派环(delegation cycle)

链状委派没有任何问题,但是形成环形链状委派也叫委派环(delegation cycle)就会导致构造死循环

例如下面这个例子Info(int)和Info(char)相互委派。

// 委派环
class Info
{
public:
  Info() :Info(1) {}
  Info(int i) : Info('c') {}
  Info(char ch) : Info(2) {}
private:
  int type{ 1 };
  char name{ 'a' };
};

在VS2022就会提醒你,这个构造函数直接或者间接委派给自己,Info的构造函数创建了一个委派周期。 所以一定要注意委派环


使用场景

构造模板

使用构造模板函数产生目标构造函数。我们创建了一个class Test,其中有一个private的构造模板,还有俩个委派构造函数,这样就可以实现动态进行构造不同的容器了。有泛式编程那味了。

#include <list>
#include <vector>
#include <deque>
using namespace std;
class Test
{
private:
  template<class T>
  Test(T first, T last):l(first, last){}
public:
  Test(vector<int>& v) :Test(v.begin(), v.end()){}
  Test(deque<int>& d) : Test(d.begin(), d.end()){}
private:
  list<int> l;
};


异常处理

在异常处理中,我们可以在委派构造函数中使用try,然后再目标构造函数中抛出异常,这将可以在委派构造函数中被捕获。

class Test
{
public:
  Test(double d)
    try : Test(1, d)
    {
    cout << "Run the body" << endl;
    }
    catch (...)
    {
      cout << "caught exception." << endl;
    }
private:
  Test(int i, double d)
  {
    cout << "going to throw" << endl;
    throw 0;
  }
  int type;
  double data;
};
int main()
{
  Test info(5);
  return 0;
}


运行结果:

目录
相关文章
|
2月前
|
编译器 C++
C++ 类构造函数初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
72 30
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
18 1
|
1月前
|
C++
C++构造函数初始化类对象
C++构造函数初始化类对象
16 0
|
1月前
|
C++
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
20 0
|
3月前
|
编译器 C++
C++的基类和派生类构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。 在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。 这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
|
5月前
|
安全 编译器 C++
C++一分钟之-构造函数与析构函数
【6月更文挑战第20天】C++中的构造函数初始化对象,析构函数负责资源清理。构造函数有默认、参数化和拷贝形式,需注意异常安全和成员初始化。析构确保资源释放,避免内存泄漏,要防止重复析构。示例代码展示了不同构造函数和析构函数的调用情况。掌握构造和析构是有效管理对象生命周期和资源的关键。
46 2
|
6月前
|
C++ Linux
|
4月前
|
编译器 C++
【C++】详解构造函数
【C++】详解构造函数
|
5月前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
5月前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。