C++11--默认成员函数控制 && 模板的可变参数

默认成员函数

之前学习C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

最重要的是前4个,默认成员函数就是我们不写编译器会生成一个默认的。在C++11中新增了两个函数:移动构造函数和移动赋值函数。


先来回顾一下,最重要的前4个默认成员函数:

类的对象被创建时,编译系统为对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作。即构造函数的作用为:初始化对象的数据成员

构造函数

  1. 无参构造函数(默认构造函数)

    • 如果没有明确写出构造函数,编译器会自动生成默认的无参构造函数,这个构造函数的函数体为空,什么也不做。
    • 如果你显式地写出了任何一个构造函数,即使是无参构造函数,编译器就不会为你生成默认构造函数。这意味着如果你想在构造函数中执行一些初始化工作,你需要显式地定义一个无参构造函数。
    • 注意,如果类中有私有成员或者需要初始化列表,编译器生成的默认构造函数可能无法满足需求,这时候也需要显式定义。
  2. 一般构造函数(重载构造函数)

    • 构造函数可以有不同的参数形式,一个类可以有多个构造函数,前提是这些构造函数的参数列表必须不同(参数个数或类型不同)。
    • 在创建对象时,编译器会根据传入的参数选择合适的构造函数。如果有多个构造函数符合条件,编译器会选择最匹配的那个,否则会产生编译错误。
    • 如果定义了任何构造函数,即使是一个重载构造函数,编译器也不会再为你生成默认构造函数。
  3. 类型转换构造函数

    • 类型转换构造函数本质上是一般的构造函数,但是它只有一个参数,并且这个参数通常是基本类型或其他简单类型。
    • 这样的构造函数允许使用内置类型或其他简单类型参数直接构造出自定义对象。
    • 为了防止不必要的隐式类型转换,可以使用explicit关键字来声明这样的构造函数,阻止隐式类型转换发生。
class A 
{
public:
    // 显式定义构造函数
    A(int a = 10) : _a(a) {}
    
    // 如果希望阻止隐式类型转换,可以加上explicit关键字
    // explicit A(int a = 10) : _a(a) {}
    
private:
    int _a;
};

int main()
{
    A a = 1; // 如果没有使用explicit,这里会发生隐式类型转换
    return 0;
}

拷贝构造函数

  1. 定义

    • 拷贝构造函数是一个特殊的构造函数,它的主要目的是根据一个已存在的对象来创建一个新的对象。
    • 拷贝构造函数的参数是一个对象的引用。
  2. 用途

    • 当需要基于已存在的对象创建一个新的对象时,拷贝构造函数会被调用。
    • 拷贝构造函数通常用于以下场景:
      • 函数返回值是类的对象时。
      • 函数参数是类的对象时(按值传递)。
      • 使用new创建对象时,需要初始化为已存在对象的拷贝。
      • 使用容器(如std::vectorstd::list等)存储类的对象时,容器内部需要拷贝对象。
  3. 默认拷贝构造函数

    • 如果没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。
    • 默认拷贝构造函数执行浅拷贝(shallow copy),即将已存在对象的数据成员逐一复制到新对象中。
    • 对于内置类型(如intdouble等),这是可以接受的。对于指针成员或其他复杂类型,如果仅仅是复制指针或引用,则会导致两个对象共享相同的资源,这就是所谓的浅拷贝问题。
    • 对于自定义类型,会调用该类型的拷贝构造函数。
  4. 深拷贝与浅拷贝

    • 浅拷贝(Shallow Copy):仅复制对象的引用或指针,而不复制其指向的数据。结果是新旧对象指向同一块内存。
    • 深拷贝(Deep Copy):不仅复制对象的引用或指针,还会复制其指向的数据,从而保证新旧对象拥有各自独立的一份数据。

拷贝赋值重载

这个重载的作用类似于拷贝构造函数:将 = 右边的类对象的值复制给 = 左边的对象,它不属于构造函数,而是一个成员函数。 = 左右两边的对象必须已经被创建。如果没有显式地写赋值重载,系统也会生成默认的赋值重载,做一些基本的拷贝工作(也是执行浅拷贝)。

移动构造函数

  1. 定义

    • 移动构造函数是一个特殊的构造函数,它用于从一个临时对象(右值)构建一个新的对象。
    • 移动构造函数通常表示为:
    class MyClass {
    public:
        MyClass(MyClass&& other); // 移动构造函数
    };
    
  2. 用途

    • 移动构造函数用于在创建新对象时,从一个临时对象(右值)中转移资源,而不是复制资源。
    • 通常在以下情况下使用移动构造函数:
      • 从函数返回值创建对象时。
      • 使用标准库容器(如std::vectorstd::list等)存储对象时。
      • 从临时对象初始化新对象时。
  3. 默认行为

    • 如果没有显式定义移动构造函数,并且没有实现析构函数、拷贝构造函数、拷贝赋值运算符中的任意一个,编译器会生成默认的移动构造函数。
    • 默认的移动构造函数完成的工作如下:
      • 对于内置类型成员,进行浅拷贝。
      • 对于自定义类型成员,调用该类型的移动构造函数。如果该类型没有移动构造函数,则调用拷贝构造函数(const左值引用类型参数)。
  4. 没有默认移动构造函数的情况

    • 如果没有生成默认移动构造函数,并且我们也没有实现移动构造函数,那么在使用一个右值构造一个对象时,会调用拷贝构造函数(即使没有显式实现,也会默认生成拷贝构造函数)。
    • 此时,即使自定义类型成员实现了移动构造函数,也不会被调用,因为拷贝构造函数只会调用自定义类型成员的拷贝构造函数。
  5. 如果你提供了移动构造,编译器不会自动提供拷贝构造。

为什么生成默认的移动构造函数,会有上述条件?

当我们显式实现 析构函数 时,代表类中包含需要特殊处理的资源(如指针、文件描述符等),应该手动释放这些资源。同样对于 拷贝构造函数拷贝赋值运算符 也需要进行深拷贝操作(显式地实现)。所以这三个函数通常是一起出现的。

一旦这三个函数出现了,代表类中包含需要特殊处理的资源(如指针、文件描述符等),就应该实现移动构造函数以确保资源的有效转移。

注: 如果显式实现了移动构造函数,就不生成默认的拷贝构造函数。

移动赋值重载

这里和 移动构造函数 类似,就不作详细介绍了。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值重载,对于内
置类型成员会执行逐成员按字节拷贝;对于自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

如果你提供了移动赋值,编译器不会自动提供拷贝赋值。


类成员变量初始化

C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值在初始化列表中进行初始化。

  1. 成员变量初始化列表

    • 在C++中,成员变量通常在构造函数的初始化列表中进行初始化。这种方式确保了成员变量在构造函数体执行之前就已经被正确初始化。
    class MyClass {
    private:
        int value;
    
    public:
        MyClass(int val) : value(val) {}
    };
    
  2. 成员变量默认初始化

    • 自C++11起,可以在类定义时直接给成员变量指定默认值,这样即使没有显式定义构造函数,编译器生成的默认构造函数也会使用这些默认值进行初始化。
    class MyClass {
    private:
        int value = 10; // 默认值
    
    public:
        MyClass() {}
    };
    

    在这个例子中,即使没有显式定义构造函数,编译器生成的默认构造函数也会使用value = 10进行初始化。

  3. 成员变量初始化的顺序

    • 成员变量的初始化顺序遵循它们在类中声明的顺序,而不是在初始化列表中的顺序。
    • 如果有多个成员变量需要初始化,它们按照在类中声明的顺序依次初始化。
    class MyClass {
    private:
        int value = 10;
        double dvalue = 20.0;
    
    public:
        MyClass() : dvalue(30.0), value(20) {} // 初始化列表
    };
    

    在这个例子中,尽管初始化列表中先初始化dvalue,然后初始化value,但实际上成员变量的初始化顺序仍然是按照它们在类中声明的顺序进行的,即先初始化value,再初始化dvalue

控制类的默认行为

强制生成默认函数的关键字default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	// 虽然显式实现了析构函数,但可以强制生成默认的移动构造函数
	Person(Person&& p) = default; 
	
	~Person()
	{}
private:
	test::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s3 = std::move(s1);
	return 0;
}

禁止生成默认函数的关键字delete

场景: 不想让别人拷贝一个类的对象。如线程类、IO流的类、锁的类等等……需要让使用者不能使用拷贝构造即可。

在C++98中,如果不想让别人使用某一个函数,方案1: 只声明函数,不实现

class A
{
public:
    A() = default;
    A(const A& a);
private:
    int a = 0;
};

int main()
{
    A a1;
    A a2 = a1;
    return 0;
}

这样调用者会出现一个链接错误,但调用者可以通过自己手动实现一个拷贝构造函数来进行构造,即:

class A
{
public:
    A() = default;
    A(const A& a);
private:
    int _a = 0;
};

// 使用者自己实现
A::A(const A& A1)
{
    _a = A1._a;
}
int main()
{
    A a1;
    A a2 = a1;
    return 0;
}

方案2: 可以将这个声明函数使用 private 修饰,这样使用者也不能手动实现完成该动作。

class A
{
public:
    A() = default;
private:
    int _a = 0;
    A(const A& a);

};

如果只将该函数设置为私有并实现了这个函数,不会生成默认的拷贝构造函数和拷贝赋值运算符。但是如果类内共有函数调用了拷贝构造,其他使用者就可以通过这个共有函数间接调用拷贝构造。

在C++11及以后的版本中,可以使用 delete 关键字来显式地删除拷贝构造函数和拷贝赋值运算符。这样可以确保编译器不会生成默认的拷贝构造函数和拷贝赋值运算符,并且用户也无法手动实现它们:

class A
{
public:
    A() = default;
    A(const A& a) = delete;
private:
    int _a = 0;
};

总结

defaultdelete 两者都用于控制类的行为。

1)delete 关键字用来禁用某些默认的成员函数。主要的作用就是禁用拷贝构造函数和拷贝赋值运算符,如下例:

class MyClass 
{
public:
    MyClass() = default;                // 使用默认构造函数
    MyClass(const MyClass&) = delete;   // 禁用拷贝构造函数
    MyClass& operator=(const MyClass&) = delete; // 禁用拷贝赋值运算符
};

2)default 关键字用于显式地指示编译器为某个成员函数生成默认的实现。它经常用于在构造函数、析构函数,以及拷贝构造函数上:

class MyClass 
{
public:
    MyClass() = default;                // 使用默认构造函数
    ~MyClass() = default;               // 使用默认析构函数
    MyClass(const MyClass&) = default;  // 使用默认拷贝构造函数
    MyClass& operator=(const MyClass&) = default; // 使用默认拷贝赋值运算符
};

补充:
使用 delete 关键字的高级操作:除了可以禁用特定的默认成员函数,delete 还可以用来禁用某些传统函数的重载

例如,你可能不希望一个整数被隐式类型转换为你的类类型:

class MyClass 
{
public:
    MyClass(int value) = delete;   // 禁用带一个整数参数的构造函数
};

模板的可变参数

C语言中的可变参数

这方面细节,可以查看博主主页中的这篇博客 “Linux – 线程池&&日志” (可变参数原理部分)~

在C语言中,printf 函数是一个变参函数(variadic function),它接受一个格式字符串作为第一个参数,后面跟着任意数量的其他参数,这些参数的类型和数量由格式字符串决定。printf 函数的这种灵活性是通过 C 语言中的变参宏(variadic macros)和运行时类型信息(RTTI,但 C 语言标准本身并不直接支持像 C++ 那样的 RTTI)的模拟来实现的,主要通过以下几个关键机制:

  1. stdarg.h 头文件:这个头文件提供了处理变参函数所需的宏和类型定义。它定义了 va_list 类型和几个宏(如 va_startva_argva_end),用于访问和处理函数参数列表中的参数。

  2. 参数传递:当调用变参函数时,除了第一个固定参数(通常是格式字符串)之外,其他参数都会被推送到调用栈上,但是编译器和运行时环境并不直接知道这些参数的类型和数量。因此,程序员需要通过格式字符串来显式地告诉 printf 如何解释这些参数。

  3. 栈的使用va_list 本质上是一个指向当前参数位置的指针(或类似的机制,取决于编译器和平台)。va_start 宏初始化这个指针,使其指向第一个可变参数。然后,va_arg 宏用于从栈中取出参数,并根据提供的类型(如 intdouble 等)调整指针,以便指向下一个参数。

  4. 类型安全和错误处理:由于 printf 和其他变参函数在编译时无法验证可变参数的类型和数量是否与格式字符串相匹配,因此这些函数类型不安全,并且可能由于参数不匹配而导致运行时错误。程序员需要仔细确保格式字符串和参数之间的匹配。

  5. 性能:虽然变参函数提供了灵活性,但它们可能会对性能产生负面影响,因为每次调用 va_arg 时都需要进行额外的计算和可能的栈操作。此外,由于类型检查在运行时进行(实际上,对于 printf 来说,没有真正的类型检查),因此错误可能在程序运行时很久之后才被发现。

C语言中的可变参数属于运行时解析,即在运行时可以通过数组解析可变参数(参数的数量和类型在编译时未知,需要在运行时确定):

// 默认传递进来的都是整数
void Test(int num, ...)
{
	va_list arg;
	va_start(arg, num);
	while(num)
	{
		int data = va_arg(arg, int); // 获取形参值
		num--;
	}
	va_end(arg); // arg = NULL
}

Test(3, 11, 22, 33);

模板的可变参数

template<class... Args>
static HeapOnly* CreateObj(Args&&... args)
{
    return new HeapOnly(args...);
}

这里主要看模板的可变参数的用法,其余后续会讲解:

  1. 模板参数包(Variadic Template Arguments)

    • template<class... Args> 表示这是一个模板函数,可以接受任意数量的类型参数 Args。这里的 ... 是变长模板参数包的语法,表示可以传递任意数量的参数。
    • Args&&... 表示将传入的参数以右值引用的形式展开。这里的 && 表示通用引用(universal reference),可以自动适配左值引用或右值引用。
  2. 返回值类型

    • 函数返回类型为 HeapOnly*,表示返回一个指向 HeapOnly 类型对象的指针。
  3. 函数体

    • return new HeapOnly(args...); 使用 new 关键字在堆上创建一个新的 HeapOnly 对象,并将传入的参数展开后传递给构造函数。args... 表示将传入的参数列表展开并传递给构造函数。

模板参数是编译时进行解析。如果想依次拿到每个参数类型和值,就需要进行编译时递归解析(因为模板参数在编译时确定),不能在运行时使用数组进行解析:

// 递归出口
void _ShowList()
{
    std::cout << std::endl;
}

template <class T, class ...Args>
void _ShowList(const T& val, Args... args)
{
    std::cout << val << " ";
    _ShowList(args...);
}

template<class ...Args>
void ShowList(Args... args)
{
    _ShowList(args...);
}

int main()
{
    ShowList();
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
    return 0;
}

底层在进行推导时,大概是这个过程:
在这里插入图片描述

注意: 如果编译时,递归层数过深,编译器可能会挂掉。

template<size_t N>
void func()
{
	std::cout << N << std::endl;
	func<N - 1>();
}

template<>
void func<1>()
{
	std::cout << 1 << std::endl;
}

int main()
{
	func<2500>();
	return 0;
}

这里编译器递归 2500 层,就会挂掉,进行报错:
在这里插入图片描述

还可以再优化一下,将 万能引用完美转发 应用到模板的参数中:

template <class T, class ...Args>
void _ShowList(T&& val, Args&&... args)
{
    std::cout << val << " ";
    _ShowList(args...);
}

template<class ...Args>
void ShowList(Args&&... args)
{
    _ShowList(args...);
}

应用emplace_back

emplace_back()是C++11中引入的一个非常有用的成员函数,主要用于向容器(如std::vectorstd::liststd::deque等)末尾添加元素。它与push_back()类似,但有几个重要的区别,使得它在某些情况下更加高效和实用。


引入

为了能理解 emplace_back() 的工作原理,这里先来分析一下 push_back 的插入过程:
(这里还是使用 std::list 和 自定义的 test::string源码链接

string(const string& s) -- 深拷贝 // 1
string(const string& s) -- 深拷贝 // 2

string(const string& s) -- 深拷贝 // 3
string(const string& s) -- 深拷贝 // 4
string(string&& s) -- 移动语义 // 5
string(string&& s) -- 移动语义 // 6 

前两行的深拷贝是由于使用 "xxx""yyy" 两次进行隐式类型转换 构造 test::string 所消耗的代价,之后再使用这两个 test::string 对象构造出 std::pair 对象。因为这里主要考虑push_back所消耗的代价,后续不再讨论。

因为 std::list::push_back() 有两个版本,所以在后续会根据传入参数为左值还是右值去调用对应的版本,进而进行不同的插入动作:
在这里插入图片描述

分析3、4步骤:

因为 kv 是左值, 所以会进行步骤:

  • push_back(kv)kv作为参数传递给std::pairpush_back左值参数方法。
  • std::pairpush_back方法会调用两次 test::string拷贝构造函数 (如,string(const string& s))来创建两个新的test::string对象,并使用这两个test::string对象构造出一个std::pair<test::string, test::string>类型对象,最终将其添加到列表中。

分析5、6步骤:

因为 std::move(kv)右值, 所以会进行步骤:

  • push_back(std::move(kv))std::move(kv) 作为参数传递给std::pairpush_back() 右值参数方法。
  • std::pairpush_back方法会调用两次 test::string移动构造函数 (如,string(const string& s))来"窃取资源"创建两个新的test::string对象,并使用这两个test::string对象构造出一个std::pair<test::string, test::string>类型对象,最终将其添加到列表中。

无效场景

如果像下方这样使用 emplace_back(),效果和使用 push_back() 效果类似:

示例一:深拷贝

需要深拷贝的类,使用 emplace_back() 插入左值还是右值,效果和使用 push_back()

int main()
{
	std::list<std::pair<test::string, test::string>> lt1;
	std::pair<test::string, test::string> kv("xxx", "yyy");
	std::cout << std::endl;

	lt1.push_back(kv);
	lt1.push_back(std::move(kv));

	std::cout << std::endl;
	std::pair<test::string, test::string> kv1("xxx", "yyy");
	lt1.emplace_back(kv1);
	lt1.emplace_back(std::move(kv1));
	return 0;
}

运行结果:

string(const string& s) -- 深拷贝
string(const string& s) -- 深拷贝 // push_back(kv)
string(string&& s) -- 移动语义
string(string&& s) -- 移动语义 // push_back(std::move(kv))

string(const string& s) -- 深拷贝
string(const string& s) -- 深拷贝 // emplace_back(kv1)
string(string&& s) -- 移动语义
string(string&& s) -- 移动语义 //emplace_back(std::move(kv1))

示例二:浅拷贝

对于浅拷贝的 匿名对象有名对象emplace_back()插入与push_back()插入代价一样:

class Date
{
public:
	// 构造函数
	Date(int year = 2024, int month = 9, int day = 22)
		:_year(year), _month(month), _day(day)
	{
		std::cout << "构造函数" << std::endl;
	}
	// 拷贝构造函数
	Date(const Date& d)
		:_year(d._year), _month(d._month), _day(d._day)
	{
		std::cout << "拷贝构造函数" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	std::list<Date> lt1;
	lt1.push_back(Date(1, 1, 1));
	std::cout << "------------------" << std::endl;
	lt1.emplace_back(Date(2, 2, 2));
	std::cout << "--------------------------------------" << std::endl;
	std::cout << std::endl;
	Date d1;
	std::cout << "--------------------------------------" << std::endl;
	lt1.push_back(d1);
	std::cout << "------------------" << std::endl;
	lt1.emplace_back(d1);
	std::cout << "--------------------------------------" << std::endl;
	return 0;
}

运行结果:

构造函数
拷贝构造函数
------------------
构造函数
拷贝构造函数
--------------------------------------

构造函数
--------------------------------------
拷贝构造函数
------------------
拷贝构造函数
--------------------------------------

在这里插入图片描述

总结

对象时,使用 emplace_back() 的插入代价和 push_back() 一样。

有效场景

而像这样使用,才能发挥 emplace_back() 真正的作用:

示例一:深拷贝

int main()
{
	std::list<std::pair<test::string, test::string>> lt1;
	lt1.push_back(std::pair<test::string, test::string>("111", "222"));
	std::cout << "--------------------------------------" << std::endl;
	lt1.emplace_back("111", "222");
	return 0;
}

运行结果:

string(char* str)构造
string(char* str)构造
string(string&& s) -- 移动语义
string(string&& s) -- 移动语义
--------------------------------------
string(char* str)构造
string(char* str)构造
  • push_back() 中,需要先使用 "111", "222" 构造出一个 pair 临时对象,再使用移动构造将临时对象的资源转移到 lt1 尾部。
  • emplace_back() 中,直接使用 "111", "222"lt1 尾部构造出一个 pair 对象。

示例二:深拷贝

int main()
{
	std::list<test::string> lt1;
	lt1.push_back("333");
	std::cout << "--------------------------------------" << std::endl;
	lt1.emplace_back("333");
	return 0;
}

运行结果:

string(char* str)构造
string(string&& s) -- 移动语义
--------------------------------------
string(char* str)构造
  • push_back() 中,需要先使用 "333" 构造出一个 test::string 临时对象,最为参数传递给push_back(),再使用移动构造将临时对象的资源转移到 lt1 尾部。
  • emplace_back() 中,直接使用 "333"lt1 尾部构造出一个 test::string 对象。
  • 补充: 第一个必须创建一个临时对象,因为 "333" 需要和 push_back 的参数const test::string&保持一致。

示例三:浅拷贝

浅拷贝的类型,emplace_back() 提升的效率较大:

class Date
{
public:
	// 构造函数
	Date(int year = 2024, int month = 9, int day = 22)
		:_year(year), _month(month), _day(day)
	{
		std::cout << "构造函数" << std::endl;
	}
	// 拷贝构造函数
	Date(const Date& d)
		:_year(d._year), _month(d._month), _day(d._day)
	{
		std::cout << "拷贝构造函数" << std::endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	std::list<Date> lt1;
	lt1.push_back({ 1, 1, 1 });
	std::cout << "--------------------------------------" << std::endl;
	lt1.emplace_back(2, 2, 2);
	return 0;
}

运行结果:

构造函数
拷贝构造函数
--------------------------------------
构造函数

注意: 不支持这种写法 emplace_back({2, 2, 2}) ,而支持这种写法 push_back({ 1, 1, 1 })。原因如下:

  • 这是因为 push_back() 的参数是确定的(模板传递的Date)void push_back (const Date& val);,因此可以进行隐式类型转换(进行构造临时对象+拷贝构造->被优化为直接构造)。
  • template <class... Args> void emplace_back (Args&&... args); 而 emplace_back 的参数是一个自定义模板,编译器无法确定其类型,会将 { 1, 1, 1 } 识别为 std::initializer_list ,而 在底层不能使用 std::initializer_list 作为参数插入到 list 的节点中。
总结

传入构造对象参数时,使用 emplace_back() 才会提升效率:

  • 对于深拷贝并实现移动构造的类,减少一次移动构造
  • 对于浅拷贝的类,减少一次拷贝构造

emplace_back() 的特点

  1. 原地构造

    • emplace_back()直接在容器的末尾构造新元素,而不是先构造一个临时对象然后再移动或复制到容器中。
    • 这意味着emplace_back()可以避免临时对象的构造和销毁过程,从而提高性能。
  2. 参数转发

    • emplace_back()接受完美转发的参数,这意味着它可以处理右值引用(R-value references),从而充分利用移动语义。
    • 这使得emplace_back()可以高效地处理临时对象和将要销毁的对象。
  3. 避免两次构造

    • push_back()相比,emplace_back()避免了两次构造的过程。使用push_back()时,首先在堆上构造一个临时对象,然后将其移动或复制到容器中,最后销毁临时对象。
    • emplace_back()直接在容器的末尾构造新对象,不需要中间步骤。

使用示例

假设我们有一个简单的类Point,并希望将其对象添加到一个std::vector中。示例代码为:

#include <iostream>
#include <vector>

class Point {
public:
    int x, y;

    Point(int x, int y) : x(x), y(y) {}

    // 重载输出运算符,用于打印对象
    friend std::ostream& operator<<(std::ostream& os, const Point& p) {
        return os << "(" << p.x << ", " << p.y << ")";
    }
};

int main() {
    std::vector<Point> points;

    // 使用 push_back() 添加元素
    points.push_back(Point(1, 2));
    // 使用 emplace_back() 添加元素
    points.emplace_back(3, 4);

    for (const auto& point : points) {
        std::cout << point << std::endl;
    }

    return 0;
}

在这个例子中,points.emplace_back(3, 4)直接在容器末尾构造了一个Point对象,而不是先构造一个临时对象再移动或复制到容器中。

emplace_back() vs. push_back()

  • push_back()

    • 先在堆上构造一个临时对象,然后将其移动或复制到容器中(移动 – 调用移动构造;复制 – 调用拷贝构造),最后销毁临时对象。
    • 这个过程可能涉及多次构造和销毁操作。
  • emplace_back()

    • 直接在容器的末尾构造新对象。
    • 避免了临时对象的构造和销毁,提高了性能。
总结
  • emplace_back() 是C++11中引入的一个成员函数,用于向容器末尾添加元素。
  • 它直接在容器的末尾构造新对象,避免了临时对象的构造和销毁。
  • emplace_back() 接受完美转发的参数,可以高效处理右值引用。
  • 相比于push_back()emplace_back() 更加高效,尤其是在处理自定义类型对象时。

通过使用emplace_back(),可以显著提高代码的性能和效率,特别是在处理大量对象或自定义类型时。

emplace 如何实现

template<class ...Args>
list_node(Args&&... args)
	: _data(std::forward<Args>(args)...), _prev(nullptr), _next(nullptr)
{}

template<class ...Args>
iterator emplace(iterator pos, Args&&... args)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	node* new_node = new node(std::forward<Args>(args)...);


	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;

	return new_node;
}


// emplace 系列函数
template<class ...Args>
void emplace_back(Args&&... args)
{
	emplace(end(), std::forward<Args>(args)...);
}

模板参数包和统一初始化列表

class HeapOnly
{
public:
    // 使用模板来创建对象,根据参数类型选择正确的构造函数
    template<typename... Args>
    static HeapOnly* CreateObj(Args&&... args)
    {
        return new HeapOnly(std::forward<Args>(args)...);
    }

private:
    HeapOnly() : _x(0), _y(0) {} // 默认构造函数

    // 构造函数接受两个 int 和一个 initializer_list<int>
    HeapOnly(int x, int y, std::initializer_list<int> list)
        : _x(x), _y(y), _a(list.begin(), list.end())
    {}

    int _x;
    int _y;
    std::vector<int> _a;
};

int main()
{
    // 正确写法
    std::initializer_list<int> arr = { 3, 4, 5 };
    HeapOnly* ho1 = HeapOnly::CreateObj(1, 2, arr);

    // 错误写法
    HeapOnly* ho2 = HeapOnly::CreateObj(1, 2, { 3, 4, 5 });
    delete ho1;
    delete ho2;

    return 0;
}

报错信息:

error C2672: “HeapOnly::CreateObj”: 未找到匹配的重载函数
error C7627: “initializer list”: 不是“Args”的有效模板参数

原因:

正确写法

在C++中,您能够使用 std::initializer_list<int> arr = { 3, 4, 5 }; 并将 arr 作为参数传递给 CreateObj 函数HeapOnly::CreateObj(1, 2, arr);,是因为这里显式地创建了一个 std::initializer_list<int> 类型的变量 arr。当您这样做时,编译器可以明确地知道 arr 的类型,并将其作为模板参数 Args... 中的一个元素进行推导。

具体来说,当您调用 CreateObj(1, 2, {3, 4, 5}) 时,编译器会看到三个参数:

  1. 一个 int 类型的值 1
  2. 一个 int 类型的值 2
  3. 一个 std::initializer_list<int> 类型的值 {3, 4, 5}

因此,Args 的类型推导结果为 int, int, std::initializer_list<int>,也就会生成一个匹配参数的CreateObj() 函数,即:

static HeapOnly* CreateObj(int x, int y, std::initializer_list<int> lt)
{
    return new HeapOnly(x, y, lt);
}

下一步就会调用对应的HeapOnly构造函数,构造对象并完成初始化工作。

HeapOnly(int x, int y, std::initializer_list<int> list)
    : _x(x), _y(y), _a(list.begin(), list.end())
{}

错误写法

然而,当您尝试直接使用 { 3, 4, 5 } 作为 CreateObj 函数的参数时,情况就变得复杂了。HeapOnly::CreateObj(1, 2, { 3, 4, 5 });

这个花括号初始化列表本身并不直接具有类型,而是依赖于上下文来确定其类型。在大多数情况下,如果上下文期望一个 std::initializer_list,那么花括号初始化列表就会被解释为一个 std::initializer_list。但是,在模板参数推导的上下文中,情况就有所不同。

模板参数推导需要确定每个模板参数的具体类型,而当您传递一个花括号初始化列表时,编译器需要决定这是否应该被推导为一个 std::initializer_list,还是其他可能的类型(如果有多个构造函数接受不同类型的参数,并且这些类型都可能与花括号初始化列表匹配的话)。在某些情况下,如果编译器无法唯一地确定模板参数的类型,它可能会报错或选择一个不期望的类型。

此外,即使编译器能够确定花括号初始化列表应该被推导为一个 std::initializer_list,它也需要确保这个推导与函数模板的其他参数类型相兼容。如果函数模板的其他参数类型与 std::initializer_list 的推导产生冲突,那么编译器也可能会报错。

总结一句就是{ 3,4,5 } 的类型是不确定的,不一定是 std::initializer_list,所以编译器无法进行推导。可以使用以下代码进行验证:

class Date
{
public:
    Date(int year = 2024, int month = 10, int day = 4)
        :_year(year),  _month(month), _day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

class HeapOnly
{
public:
    // 使用模板来创建对象,根据参数类型选择正确的构造函数
    template<typename... Args>
    static HeapOnly* CreateObj(Args&&... args)
    {
        return new HeapOnly(std::forward<Args>(args)...);
    }

    static HeapOnly* CreateObj(int x, int y, Date d)
    {
        return new HeapOnly(x, y, d);
    }

    static HeapOnly* CreateObj(int x, int y, std::initializer_list<int> list)
    {
        return new HeapOnly(x, y, list);
    }

private:
    HeapOnly() : _x(0), _y(0) {} // 默认构造函数

    // 构造函数接受两个 int 和一个 initializer_list<int>
    HeapOnly(int x, int y, std::initializer_list<int> list)
        : _x(x), _y(y), _a(list.begin(), list.end())
    {}

    HeapOnly(int x, int y, Date d)
        : _x(x), _y(y), _d(d)
    {}

    int _x;
    int _y;
    std::vector<int> _a;
    Date _d;
};

当我们同样使用{ 3, 4, 5 }进行初始化时:

int main()
{
    HeapOnly* ho2 = HeapOnly::CreateObj(1, 2, { 3, 4, 5} );
    delete ho2;
    return 0;
}
  • CreateObj(1, 2, { 3, 4, 5} ) 会优先匹配参数为int x, int y, std::initializer_list<int> listCreateObj函数,因为这里优先将{ 3, 4, 5}看作std::initializer_list<int>类型。
  • 如果CreateObj函数没有参数为int x, int y, std::initializer_list<int> list的版本,则会先调用Date类的构造函数,利用{ 3, 4, 5}构造一个Date对象,然后匹配参数为int x, int y, Date dCreateObj函数。
  • 如果上述两个版本的CreateObj函数,只有CreateObj(Args&&... args)版本的该函数,编译器就不知道将{ 3, 4, 5}推导为什么类型,就直接报错(注意,这里它不会默认推导为std::initializer_list<int>类型,编译器担心会推导错误,所以就直接报错)。

今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值