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

news2024/11/24 18:37:39

默认成员函数

之前学习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>类型,编译器担心会推导错误,所以就直接报错)。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2197011.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL连接:内连接

先看我的表结构 dept表 emp表 内连接分为两个连接方式 1.隐式内连接 2.显式内连接 1.隐式内连接 基本语法 select 字段列表 FROM 表1, 表2 WHERE 条件... ;例子&#xff1a;查询每一个员工的姓名&#xff0c;及关联的部门的名称&#xff08;隐式内连接实现&#xff09; …

Pikachu-url重定向-不安全的url跳转

不安全的url跳转 不安全的url跳转问题可能发生在一切执行了url地址跳转的地方。如果后端采用了前端传进来的(可能是用户传参,或者之前预埋在前端页面的url地址)参数作为了跳转的目的地,而又没有做判断的话就可能发生"跳错对象"的问题。 url跳转比较直接的危害是: …

LSTM模型变种

LSTM模型变种 一、GRU 1.什么是GRU GRU&#xff08;Gated Recurrent Unit&#xff09;是一种循环神经网络&#xff08;RNN&#xff09;的变体&#xff0c;它被设计用来解决传统RNN在处理长序列时可能遇到的梯度消失或梯度爆炸问题。GRU通过引入门控机制来控制信息的流动&…

PCL 计算点云的高斯曲率

目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.1.1 法向量计算 2.1.2 主曲率和高斯曲率计算 2.1.3 可视化函数 2.2完整代码 三、实现效果 PCL点云算法汇总及实战案例汇总的目录地址链接&#xff1a; PCL点云算法与项目实战案例汇总&a…

1000万元试水,看完AI约稿平台上赚钱的故事,真的心动了……

在生成对抗网络&#xff08;GAN&#xff09;、扩散模型&#xff08;Diffusion Models&#xff09;、视觉语言预训练模型&#xff08;CLIP&#xff09;等技术的发展下&#xff0c;AI 绘画可做的内容越来越多。 建筑设计、服装设计、室内设计、插画设计等垂类模型的出现也让更多…

从理论到实践:AI智能分析网关V4烟火检测算法的应用场景探索

在信息化和智能化的今天&#xff0c;AI智能分析网关V4作为一款集成了先进技术的硬件设备&#xff0c;在烟火检测领域展现出了强大的应用价值。本文将详细阐述AI智能分析网关V4烟火检测算法的原理及其在各种场景中的应用。 一、AI智能分析网关V4烟火检测算法原理 深度学习基础…

激光测距用高精度时间测量(TDC)电路MS1003,比 MS1002 具有更高的精度和更小的封装,适合于高精度小封装的应用领域

MS1003 是一款高精度时间测量 (TDC) 电路&#xff0c;对比 MS1002 具 有更高的精度和更小的封装&#xff0c;适合于高精度小封装的应用领域。 MS1003 具有双通道、多脉冲的采样能力、高速 SPI 通讯、 多种测量模式&#xff0c;适合于激光雷达和激光测距。 主要特点 …

word 无法进入修订模式

word 无法进入修订模式&#xff0c;原来是被保护了&#xff0c;取消保护却没有密码 方法&#xff1a; 1、新建-空白文档 2、插入-对象-文件中的文字...&#xff0c;然后选择受保护的文档。这样即可把受保护的文档克隆一份到新文档。新文档当然就可以进入修订模式了。

Mac上最好用的快捷回复工具-快捷短语

网络上打字回复已经成为我们日常生活中不可缺少的一件事了&#xff0c;当有的时候需要一遍又一遍的回复重复的内容的时候&#xff0c;难免会感到疲惫&#xff0c;每次复制粘贴重复的内容&#xff0c;时间一长真的会很让人抓狂。 这里给大家推荐一款很好用的快捷回复工具&#…

全局变量的重复定义会怎样?

有些人的第一反应是编译不过吧&#xff1f; //fun.c void func() {printf("测试1"); }//main.c void func() {printf("测试2"); } void main() {func(); } 编译&#xff1a; 这里可以看到保存错了&#xff0c;因为func重复定义了。 但是重复定义就会全部…

DNSlog注入原理

DNSlog注入 dnslog注入也相当于盲注的一种&#xff0c;但是比盲注效率高一点。 在一定的情况下&#xff0c;如果我们想对一个网站使用布尔或时间盲注&#xff0c;手动注入会对WAF的绕过会非常有帮助&#xff0c;但是效率不高&#xff0c;因此我们往往会使用SQLmap等工具注入&a…

十款主流图纸加密软件推荐|有效防止图纸泄密

在设计和工程行业&#xff0c;保护设计图纸的安全性至关重要。随着信息技术的发展&#xff0c;数据泄密事件频发&#xff0c;选择合适的图纸加密软件成为了企业和个人保护知识产权的重要手段。本文将推荐十款主流的图纸加密软件。 1. Ping32 Ping32是一款专注于图纸和设计文件…

「完美收官」科东软件2024上海国际工博会精彩回顾:鸿道操作系统赋能新型工业化

第24届中国国际工业博览会已经落下帷幕。历经24届&#xff0c;工博会已成为全球工业发展的“风向标”。今年的中国工博会以“工业聚能 新质领航”为主题&#xff0c;工业自动化、数控机床与金属加工、新能源与智能网联汽车、机器人等领域重磅展品悉数亮相&#xff0c;展示了中国…

STM32-TIM输入捕获

一、概述 IC&#xff08;Input Capture&#xff09;输入捕获 输入捕获模式下&#xff0c;当通道输入引脚出现指定电平跳变&#xff08;上升沿或下降沿&#xff09;时&#xff0c;当前CNT的值将被锁存到CCR中&#xff0c;可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续…

Label Studio 半自动化标注

引言 Label Studio ML 后端是一个 SDK,用于包装您的机器学习代码并将其转换为 Web 服务器。Web 服务器可以连接到正在运行的 Label Studio 实例,以自动执行标记任务。我们提供了一个示例模型库,您可以在自己的工作流程中使用这些模型,也可以根据需要进行扩展和自定义。 1…

厨房用品分割系统源码&数据集分享

厨房用品分割系统源码&#xff06;数据集分享 [yolov8-seg-C2f-DCNV3&#xff06;yolov8-seg-AFPN-P345等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challenge 项目来源AAAI Global Al ln…

C++欧拉函数

题目一 求欧拉函数 解题思路 分解质因数&#xff1b;代入公式计算即可&#xff08;注意要防止计算出小数是结果不准&#xff09;&#xff1b; 代码实现 #include<iostream> #include<algorithm> #include<cmath>using namespace std;void Euler(int n) {i…

注册安全分析报告:惠农网

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

丝杆支撑座预压标准解析

丝杆支撑座预压的主要目的是提高轴的旋转精度、刚性和运行性能&#xff0c;同时防止轴在运转过程中产生震动和异响&#xff0c;从而提高系统的整体精度和稳定性。那么&#xff0c;丝杆支撑座的预压标准是什么呢&#xff1f; 丝杆支撑座的预压可以分为标准型轻预压和标准型重预压…

atcoder-374(a-e)

atcoder-374 文章目录 atcoder-374ABC简洁的写法正解 D正解 E A #include<bits/stdc.h>using namespace std;signed main() {string s;cin>>s;string strs.substr(s.size()-3);if(str "san") puts("Yes");else puts("No");return 0…