C11 列表初始化、左/右值引用、移动语义、可变参数模版

2024-04-05 21:44

本文主要是介绍C11 列表初始化、左/右值引用、移动语义、可变参数模版,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一、统一的列表初始化

1、{}初始化

2、std::initializer_list

3、模拟实现vector花括号操作

二、声明

1、自动类型推断 - auto

2、类型推导关键字 - decltype

3、空指针常量 - nullptr

4、范围for循环

三、右值引用和移动语义

1、左值引用和右值引用

左值与左值引用

右值与右值引用

2、左值引用与右值引用比较

3、右值引用使用场景

4、左值引用的使用场景:

5、左值引用的短板:

6、右值引用和移动语义:

7、移动赋值:

8、右值引用引用左值及其一些更深入的使用场景分析

9、完美转发

10、模拟实现list push_back右值引用

完整代码:

关键讲解:

四、新的类功能

1、默认成员函数

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

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

4、emplace

五、可变参数模板

1、概念

2、递归函数方式展开参数包

3、逗号表达式展开参数包


 

一、统一的列表初始化

1、{}初始化

在C++98中,标准已经规定可以利用花括号{}对数组或结构体成员进行统一的初始化操作。例如:
struct Point
{int _x;int _y;
};int main()
{// 对数组进行初始化int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };// 对结构体进行初始化Point p = { 1, 2 };return 0;
}

随着C++11标准的推出,初始化列表的使用范围得到了显著扩大,不仅限于内置类型和结构体,而且适用于所有用户自定义类型。在使用初始化列表时,既可以包含等号(=),也可以省略。

struct Point
{int _x;int _y;
};int main()
{// C++11中对基本类型的列表初始化int x1 = 1;int x2{ 2 };// 对数组进行列表初始化int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };// 对结构体进行列表初始化Point p{ 1, 2 };// C++11中,列表初始化还可应用于new表达式中动态创建数组int* pa = new int[4]{ 0 };// 列表初始化方式在创建对象时同样适用,并会调用相应的构造函数进行初始化class Date{public:Date(int year, int month, int day): _year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}private:int _year;int _month;int _day;};// 老式的构造函数调用方式Date d1(2022, 1, 1);// C++11支持的列表初始化形式,这将调用构造函数进行初始化Date d2{ 2022, 1, 2 };Date d3 = { 2022, 1, 3 };return 0;
}

2、std::initializer_list

std::initializer_list 是C++11引入的一种特殊类型,它能够存储一组指定类型的常量值序列,并提供迭代器访问这些值。下面是一个示例说明其类型:

int main()
{// il 的类型即为 std::initializer_list<int>auto il = { 10, 20, 30 };cout << typeid(il).name() << endl;return 0;
}

std::initializer_list 主要用于简化构造函数参数的传递,尤其是在初始化容器对象时。C++11标准库中的许多容器如 std::vectorstd::liststd::map 等都增加了接受 std::initializer_list 类型参数的构造函数,使得我们可以方便地用花括号列表来初始化容器。

例如:

int main()
{// 使用 std::initializer_list 初始化容器std::vector<int> v = { 1, 2, 3, 4 };std::list<int> lt = { 1, 2 };// 使用 std::initializer_list 初始化 map,其中每个元素都会被解释为 pairstd::map<std::string, std::string> dict = { { "sort", "排序" }, { "insert", "插入" } };// 同样支持大括号对容器进行赋值操作v = { 10, 20, 30 };return 0;
}

3、模拟实现vector花括号操作

为了使我们自定义的 `bit::vector` 支持花括号初始化以及赋值操作,可以如下实现:

namespace bit
{template <class T>class vector {public:typedef T* iterator;// 构造函数接收 std::initializer_list 参数vector(std::initializer_list<T> l){_start = new T[l.size()];_finish = _start + l.size();_endofstorage = _start + l.size();iterator vit = _start;typename std::initializer_list<T>::iterator lit = l.begin();// 遍历并复制 initializer_list 中的元素到新建的 vector 中while (lit != l.end()) {*vit++ = *lit++;}}// 赋值运算符重载,接收 std::initializer_list 参数vector<T>& operator=(std::initializer_list<T> l) {// 创建临时对象,并用新的 initializer_list 初始化vector<T> tmp(l);// 通过 swap 技术完成赋值(避免自我赋值问题)std::swap(_start, tmp._start);std::swap(_finish, tmp._finish);std::swap(_endofstorage, tmp._endofstorage);return *this;}private:iterator _start;iterator _finish;iterator _endofstorage;};
}

构造函数

  1. 动态分配足够存储 l 中元素的连续内存空间。
  2. 初始化 _start 指针指向开始位置,_finish 和 _endofstorage 指针指向结束位置(这里假设没有预留额外空间)。
  3. 使用迭代器遍历 initializer_list,并将其中的元素依次复制到新分配的内存空间内。

赋值运算符重载

  1. 创建一个临时 vector<T> 对象 tmp,并使用 l 初始化它。
  2. 通过 std::swap() 函数交换当前对象(*this)和临时对象 tmp 的内部数据成员 _start_finish 和 _endofstorage。这样,原先对象的数据就被临时对象的新数据所取代。
  3. 返回当前对象的引用 *this,以便支持链式赋值。

二、声明

在C++11中,引入了多种简化声明的方式,特别是在模板编程中大大提高了效率和可读性。

1、自动类型推断 - auto

自动类型推断 - auto 在C++98中,auto关键字表示变量具有局部作用域和自动存储期,但由于局部变量默认就是自动存储类型,所以当时的auto显得并不重要。然而,在C++11中,auto的含义发生了改变,它被用来实现自动类型推断。这意味着当你声明一个变量并使用auto时,编译器会根据初始化表达式自动推断出该变量的类型,因此必须进行显式初始化。

示例:

int main()
{int i = 10;auto p = &i;  // p 的类型被推断为 int*auto pf = strcpy; // pf 的类型被推断为 char* (*)(char*, const char*)cout << typeid(p).name() << endl;cout << typeid(pf).name() << endl;map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };// 使用auto简化迭代器声明// map<string, string>::iterator it = dict.begin();auto it = dict.begin(); // it 的类型被推断为 map<string, string>::iteratorreturn 0;
}

2、类型推导关键字 - decltype

`decltype` 关键字允许你声明变量的类型与指定表达式的结果类型相同,这对于理解复杂的模板类型或推断函数返回类型极其有用。

// decltype 的应用场景
template <class T1, class T2>
void F(T1 t1, T2 t2)
{decltype(t1 * t2) ret; // ret 的类型由 t1 和 t2 相乘决定cout << typeid(ret).name() << endl;
}int main()
{const int x = 1;double y = 2.2;decltype(x * y) ret; // ret 的类型被推断为 doubledecltype(&x) p;      // p 的类型被推断为 int*cout << typeid(ret).name() << endl;cout << typeid(p).name() << endl;F(1, 'a'); // 根据传入参数推断 ret 的类型return 0;
}

3、空指针常量 - nullptr

在C++98及之前版本中,通常使用宏`NULL`表示空指针,但`NULL`被定义为整数值0,这可能会引发混淆,因为在某些上下文中0可以同时代表指针常量和整数值。为了解决这个问题并提高代码清晰度和安全性,C++11引入了`nullptr`关键字,专用于表示空指针。
// 在C++11及以上版本中,建议使用nullptr代替NULL
void* ptr = nullptr;

4、范围for循环

范围for循环 C++11引入了范围for循环,这是一种更简洁的方式来遍历任何可迭代的对象,包括但不限于STL容器。它的语法格式如下:

for (declaration : range-expression)statement

例如,对于上述提到的dict容器,我们可以用范围for循环遍历:

for (const auto& pair : dict)
{cout << pair.first << ": " << pair.second << endl;
}

在这个循环中,编译器会自动获取dict的迭代器,并按顺序遍历容器中的每个元素,无需手动管理迭代器。每次迭代时,pair会被隐式声明为一个引用,引用当前迭代到的键值对。

三、右值引用和移动语义

1、左值引用和右值引用

在C++11中,引入了右值引用的概念,对于我们之前熟悉的引用,现在称之为左值引用。无论是左值引用还是右值引用,它们本质上都是为对象取了一个别名。

左值与左值引用

左值是指可以出现在赋值符号左侧的表达式,例如变量名或者解引用后的指针。它们可以有持续的地址,并可以被赋新的值。特别地,被const修饰后的左值不能被重新赋值,但我们仍可以获取其地址。左值引用正是为这些左值取的一个别名。

示例代码:

int main() {int* p = new int(0); // p是左值int b = 1; // b是左值const int c = 2; // c是左值,虽然不能赋新值,但可以取地址// 下面是对左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p; // 解引用得到左值return 0;
}

右值与右值引用

右值通常是不能出现在赋值符号左侧的表达式,比如字面量、表达式的计算结果或者是函数的返回值(此处不包括返回左值引用的情况)。右值通常不能取地址。右值引用就是对这些右值的引用,为它们取了别名。

示例代码:

int main() {double x = 1.1, y = 2.2;// 下面是常见的右值10;x + y;fmin(x, y);// 下面是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);// 下面的尝试会引发编译错误,因为右值不能出现在赋值符号左侧// 10 = 1;// x + y = 1;// fmin(x, y) = 1;return 0;
}

需要注意,右值本身是不能取地址的,但一旦我们为右值创建了一个右值引用(相当于给右值取了一个别名),这个引用的右值就可以被存储在一个具体位置,从而可以获取其地址,并且可以被修改。如果我们希望防止这种修改,可以通过使用const修饰的右值引用来实现。

int main() {double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20;    // 正确,可以修改rr1rr2 = 5.5;   // 错误,不能修改const修饰的右值引用return 0;
}

虽然右值引用的概念可能初看起来比较抽象,但它在现代C++编程中,尤其是实现移动语义和优化临时对象的处理方面发挥着重要作用。

2、左值引用与右值引用比较

左值引用要点:

  1. 左值引用(non-const左值引用)仅能绑定到持久的、可命名的左值对象,例如变量名、数组元素等。例如,在例子中,我们能成功创建一个引用ra1指向左值变量a,但不能将一个左值引用绑定到右值(如字面量10)上。

    int a = 10;
    int& ra1 = a; // 正确,ra1成为了a的别名
    // int& ra2 = 10; // 错误,无法将右值绑定到左值引用
  2. 然而,const左值引用则具有更高的灵活性,它既能绑定到左值对象,也能绑定到右值。在示例中,我们可以创建一个const左值引用ra3绑定到右值字面量10,同时也能绑定到左值变量a

    const int& ra3 = 10; // 正确,ra3绑定到右值
    const int& ra4 = a; // 正确,ra4成为a的const别名

右值引用要点:

  1. 右值引用专为右值设计,它只能绑定到临时对象或者将要销毁的对象(即右值)。例如,我们可以创建一个右值引用r1直接绑定到右值字面量10上。

    int&& r1 = 10; // 正确,r1绑定到右值
  2. 未经特殊处理,右值引用不能直接绑定到左值对象。在示例中,尝试将右值引用r2绑定到左值变量a会导致编译错误。

    int a = 10;
    // int&& r2 = a; // 错误,无法将左值绑定到右值引用
  3. 但是,通过使用std::move函数,我们可以将左值转换为将要销毁的右值(称为xvalue),进而允许右值引用绑定到这个“移动后”的左值。在示例中,std::move(a)将左值变量a转化为可被右值引用r3绑定的右值。

    int&& r3 = std::move(a); // 正确,std::move将左值a转变为可被右值引用绑定的状态
  • std::move函数是C++11引入的一个右值强制转换操作,它并不真正执行移动操作,而是提供了一种方式告诉编译器:“我打算把这个对象当作即将销毁的临时对象来处理”。在语义上,std::move将左值转换为右值引用(xvalue),这是一种特殊的右值,表示对象可以(或即将)被移动。
  • 具体来说,当我们调用std::move(some_object)时,函数并不会改变some_object的实际内容或状态,它仅仅是将some_object的类型转换为对应类型的右值引用。这样做的目的是为了配合移动构造函数或移动赋值运算符,使得类实例能够高效地转移资源所有权,而非复制资源。

3、右值引用使用场景

在C++编程中,左值引用虽然能在很多场景下有效提升效率,例如作为函数参数传递和作为函数返回值,从而避免不必要的对象拷贝,但仍然存在一定的局限性。当函数返回一个局部变量时,由于局部变量在函数结束后会销毁,此时无法使用左值引用返回,只能采用传值返回。这将导致至少一次拷贝构造(在旧版编译器中可能需要两次拷贝构造),如bit::to_string函数所示。

bit::string to_string(int value)
{bool flag = true;if (value < 0){flag = false;value = 0 - value;}bit::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;
}

为了解决这个问题,C++11引入了右值引用的概念,配合移动语义,极大地提升了资源管理的效率。右值引用允许我们绑定到即将销毁的临时对象或局部变量上,通过移动构造函数,可以将临时对象的资源转移给新创建的对象,而无需进行深度拷贝。

bit::string类中,我们添加了移动构造函数和移动赋值运算符,它们接受一个右值引用参数,并通过swap函数巧妙地将参数对象的资源转移到当前对象中。这样一来,当调用bit::to_string函数时,返回的临时字符串对象可以通过移动构造传递给接收变量,避免了深拷贝构造过程,从而显著提升了性能。

namespace bit
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){//cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}// 移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动语义" << endl;swap(s);}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}//string operator+=(char ch)string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}

总结起来,左值引用虽能在一定程度上减少对象拷贝,但对于即将销毁的临时对象,其优势无法发挥。而右值引用和移动语义正好弥补了这一短板,使得临时对象的资源能够高效地转移给其他对象,这对于包含大量数据的大对象尤其重要,能够显著降低资源开销和提升程序性能。

4、左值引用的使用场景:

左值引用在C++中的应用场合主要体现在两个方面,以提高程序运行效率:

作为函数参数: 在函数调用过程中,对于大型对象(如字符串或其他自定义类型)作为参数传递时,采用左值引用可以显著减少不必要的复制开销。例如,对比下面两个函数声明:

// 使用值传递,函数内部会创建字符串对象副本
void func1(bit::string s);
// 使用左值引用传递,避免了对象的拷贝操作
void func2(const bit::string& s);
  • main函数中调用func1(s1)func2(s1)时,func2通过引用传递s1,避免了对原始字符串对象的深拷贝,从而提升了效率。

作为函数返回值: 在设计类的成员函数时,尤其是涉及到修改原对象状态的运算符重载,返回左值引用可以避免不必要的对象复制。例如,对于字符串的追加操作:

// 使用值传递,函数内部会创建字符串对象副本
void func1(bit::string s);
// 使用左值引用传递,避免了对象的拷贝操作
void func2(const bit::string& s);
  • main函数中,使用= +=操作符对s1进行字符追加时,如果operator+=返回左值引用,那么可以直接在原s1对象上修改,无需创建新的临时对象,从而提高了效率。

5、左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,
只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,
传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)
namespace bit
{// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}    bit::string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}bit::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}
int main()
{// 在bit::string to_string(int value)函数中可以看到,这里// 只能使用传值返回,传值返回会导致至少1次拷贝构造//(如果是一些旧一点的编译器可能是两次拷贝构造)。bit::string ret1 = bit::to_string(1234);bit::string ret2 = bit::to_string(-1234);return 0;
}
  • main函数中,bit::to_string(1234)bit::to_string(-1234)分别调用后返回的是局部变量(右值),由于这些局部变量在函数结束时会超出作用域,因此不能直接使用左值引用返回。在这种情况下,只能选择传值返回。然而,传值返回通常会导致至少一次拷贝构造(对于一些旧版编译器可能需要两次)。
  • to_string函数返回的右值被用来构造ret1ret2两个对象。如果没有提供移动构造函数,编译器会默认调用拷贝构造函数来完成这一操作,因为const左值引用是可以引用右值的,这里就是一个深拷贝。 

 

6、右值引用和移动语义:

bit::string类中添加移动构造函数后,我们可以看到其基本原理是通过“窃取”参数右值的资源来构造自身,而无需进行深拷贝。因此,这种行为被称为移动构造。
// 移动构造
string(string&& s):_str(nullptr), _size(0), _capacity(0)
{cout << "string(string&& s) -- 移动语义" << endl;swap(s);
}
int main()
{bit::string ret2 = bit::to_string(-1234);return 0;
}

再运行上面bit::to_string的调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

​namespace bit
{class string{public://……// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 移动构造string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "string(string&& s) -- 移动语义" << endl;swap(s);}//……};
}bit::string to_string(int value)
{//……return str;
}int main()
{bit;:string ret2 = bit::to string(-1234);return 0;
}
  • 移动构造函数接收一个右值引用string&& s作为参数,当以一个临时对象或者将要被移动的(即将销毁的)对象来创建新对象时会被调用,实现资源的高效转移,即移动语义。在此函数中,同样输出提示信息,并直接与传入的右值对象进行内容交换。
  • main()函数中,bit::to_string(-1234)函数返回一个临时bit::string对象(这是一个右值)。当此右值用于初始化ret2时,编译器会选择最适合的构造函数进行匹配。由于存在移动构造函数且其参数类型更匹配右值引用,因此编译器会优先调用移动构造函数,从而实现了移动语义,即有效地将临时对象的所有权和资源转移给ret2,而无需进行代价较高的深拷贝操作。

7、移动赋值:

bit::string类中新增了移动赋值成员函数,它接受一个右值引用string&& s作为参数,在执行赋值操作时能够“窃取”源对象的资源而非复制它们,从而提升性能。
// 移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this;
}
int main()
{bit::string ret1;ret1 = bit::to_string(1234);return 0;
}
// 运行结果:
// string(string&& s) -- 移动构造
// string& operator=(string&& s) -- 移动赋值

main()函数中,首先声明了一个bit::string类型的变量ret1但未初始化。接着,尝试将bit::to_string(1234)返回的临时右值对象赋给ret1。这里的过程实际上分为两步:

  1. bit::to_string(1234)函数内部创建了一个临时bit::string对象,这个临时对象通过移动构造函数初始化,编译器智能地识别出它是右值,因此触发了移动构造函数的调用,输出“string(string&& s) -- 移动构造”。

  2. 随后,这个临时对象作为右值被赋值给ret1。此时,由于ret1是一个已存在的对象,编译器不再调用移动构造函数,而是调用移动赋值运算符重载函数,将临时对象的所有资源转移给ret1,同时输出“string& operator=(string&& s) -- 移动赋值”。

  • 这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象 接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。
STL中的容器都是增加了移动构造和移动赋值:
在C++ Standard Template Library (STL)中,容器类如std::vectorstd::liststd::dequestd::setstd::mapstd::unordered_setstd::unordered_map等都支持移动语义,这是从C++11标准开始引入的一个重要特性。移动构造和移动赋值是实现移动语义的关键部分,它们极大地提高了内存管理和容器操作的效率。
  1. 移动构造函数

    • 语法形式通常为:ClassName(ClassName&& other);
    • 当一个对象被当作右值引用传递给另一个新建对象时(例如返回临时对象或者用一个即将销毁的对象初始化新对象),移动构造函数会被调用。
    • 在容器中,如果插入一个临时对象或者用一个将要离开作用域的对象来扩充容器,则容器会调用元素类型的移动构造函数来构建新的容器内元素,而不是通过复制构造函数,这样可以避免深拷贝带来的高昂开销,特别是对于那些含有大量数据或不可复制资源(如文件句柄、套接字等)的对象非常有用。
  2. 移动赋值运算符

    • 语法形式通常为:ClassName& operator=(ClassName&& other);
    • 当一个对象被右值引用赋值时,移动赋值运算符会被调用,它会接管右侧对象的所有资源(如内存空间或其它资源),而释放掉左侧对象原有的资源。
    • 在STL容器中,当容器进行扩容、插入元素或修改容器内容时,如果涉及到了已有元素的移动,而不是简单的复制,那么容器内的元素类型必须支持移动赋值。例如,当std::vector容器需要重新分配内存时,它可以通过移动赋值运算符将原有元素移动到新内存区域,而不是逐一复制,这样显著提高了容器的性能。

通过支持移动构造和移动赋值,STL容器能够在不牺牲性能的前提下,更加有效地管理底层的数据存储,尤其是在处理大型对象或包含资源管理的对象时,能够避免不必要的资源拷贝,从而大大提升程序的运行效率。

8、右值引用引用左值及其一些更深入的使用场景分析

在C++中,右值引用(rvalue reference)设计之初主要是为了能够引用右值,也就是临时对象或即将销毁的对象,以实现高效的资源转移。然而,并非绝对禁止右值引用引用左值;在某些特定场景下,确实可以通过std::move函数将左值“转换”成右值引用以便实现移动语义。

C++11标准库中的std::move函数,位于头文件<memory>中,虽然它的名称可能会令人误解,但它实际上并不会真正执行任何对象的移动操作。std::move的主要功能是将一个左值强制转化为右值引用类型,这样编译器在进行类型推断和匹配时,会优先考虑调用移动构造函数或移动赋值运算符。

template<class _Ty>
typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{// 强制类型转换,使左值看上去像右值一样可移动return static_cast<typename remove_reference<_Ty>::type&&>(_Arg);
}int main()
{bit::string s1("hello world"); // s1 是左值// 这里调用拷贝构造函数,创建s2时复制了s1的资源bit::string s2(s1);// 使用std::move将左值s1转换为右值引用,此时编译器会尝试调用移动构造函数// 将s1的内部资源转移给s3,之后s1可能被置为空或处于无效状态bit::string s3(std::move(s1));// 注意:在此之后不应继续使用s1,因为它可能已不再拥有有效资源return 0;
}
  • std::move函数允许我们在适当的时候“标记”一个左值为可移动状态,促使编译器调用移动构造函数或移动赋值运算符,从而提高程序效率。然而,需要注意的是,一旦左值经过std::move转化后进行了资源转移,除非类特别设计允许,否则一般情况下原左值将不再持有有效的资源,不宜再被正常使用。
STL容器插入接口函数也增加了右值引用版本:
void push_back(value_type&& val);
int main()
{list<bit::string> lt;bit::string s1("1111");// 这里调用的是拷贝构造lt.push_back(s1);// 下面调用都是移动构造lt.push_back("2222");lt.push_back(std::move(s1));return 0;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义

9、完美转发

在C++中,模板参数中的T&&被称为万能引用(Universal Reference),它并不单纯表示右值引用,而是根据模板实参推断的结果决定其具体类型。这种设计使得万能引用既可以绑定到左值也可以绑定到右值
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}
int main()
{//标注类型PerfectForward(10);           // 右值int a;PerfectForward(a);            // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b);      // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

尽管有四个重载版本的Fun函数分别对应左值引用、const左值引用、右值引用和const右值引用,但是在PerfectForward模板函数中,参数t的类型推导会受到模板类型别名T&&的影响。这里使用了所谓的万能引用(universal reference),在模板函数中,T&&并不是总是代表右值引用,而是在不同的上下文中表现出不同的行为。

  • 当传入的是临时对象或字面量常量(如PerfectForward(10))时,T会被推导为int类型,T&&表现为右值引用,理论上应该调用Fun(int&& x)版本。但在本例中并没有发生这种情况,原因在于Fun(t)直接传入t,没有使用std::forward<T>(t),因此t失去了作为右值引用的能力,被视为左值引用,所以调用了Fun(int& x)版本。

  • 对于其他非临时变量(如ab),同样道理,尽管使用了std::move(a)std::move(b),但在PerfectForward函数内部没有进行完美转发,因此t始终被视为左值引用。即使ab通过std::move转换为了右值引用,但在这里并没有保持这个属性,因此分别调用了Fun(int& x)Fun(const int& x)版本。

std::forward 完美转发在传参的过程中保留对象原生类型属性
std::forward是C++模板编程中的关键工具,用于实现完美转发(perfect forwarding)。它在函数参数传递过程中,能够精确地保持原对象的类型属性,无论是左值还是右值引用,或是常量限定性。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{Fun(std::forward<T>(t));
}
int main()
{PerfectForward(10);           // 右值int a;PerfectForward(a);            // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b);      // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

10、模拟实现list push_back右值引用

完整代码:

#pragma once
#include<assert.h>namespace bit
{template<class T>struct list_node{list_node<T>* _next;list_node<T>* _prev;T _data;list_node(const T& x = T()):_next(nullptr), _prev(nullptr), _data(x){}list_node(T&& x = T()):_next(nullptr), _prev(nullptr), _data(forward<T>(x)){}};// 1、迭代器要么就是原生指针// 2、迭代器要么就是自定义类型对原生指针的封装,模拟指针的行为template<class T, class Ref, class Ptr>struct __list_iterator{typedef list_node<T> node;typedef __list_iterator<T, Ref, Ptr> self;node* _node;__list_iterator(node* n):_node(n){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}self& operator++(){_node = _node->_next;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self& operator--(){_node = _node->_prev;return *this;}self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const self& s){return _node != s._node;}bool operator==(const self& s){return _node == s._node;}};template<class T>class list{typedef list_node<T> node;public:typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;iterator begin(){return iterator(_head->_next);}const_iterator begin() const{return const_iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator end() const{return const_iterator(_head);}void empty_init(){_head = new node(T());_head->_next = _head;_head->_prev = _head;}list(){empty_init();}template <class Iterator>list(Iterator first, Iterator last){empty_init();while (first != last){push_back(*first);++first;}}void swap(list<T>& tmp){std::swap(_head, tmp._head);}list(const list<T>& lt){empty_init();list<T> tmp(lt.begin(), lt.end());swap(tmp);}// lt1 = lt3list<T>& operator=(list<T> lt){swap(lt);return *this;}~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();while (it != end()){//it = erase(it);erase(it++);}}void push_back(const T& x){insert(end(), x);}void push_back(T&& x){insert(end(), forward<T>(x));}void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}void insert(iterator pos, const T& x){node* cur = pos._node;node* prev = cur->_prev;node* new_node = new node(x);prev->_next = new_node;new_node->_prev = prev;new_node->_next = cur;cur->_prev = new_node;}void insert(iterator pos, T&& x){node* cur = pos._node;node* prev = cur->_prev;node* new_node = new node(forward<T>(x));prev->_next = new_node;new_node->_prev = prev;new_node->_next = cur;cur->_prev = new_node;}iterator erase(iterator pos){assert(pos != end());node* prev = pos._node->_prev;node* next = pos._node->_next;prev->_next = next;next->_prev = prev;delete pos._node;return iterator(next);}private:node* _head;};
}

关键讲解:

template<class T>
class list
{
private:struct list_node{list_node(T&& x): _data(forward<T>(x)){}T _data;// ... 其他成员和构造函数省略};public:void push_back(T&& x){insert(end(), forward<T>(x));}private:void insert(iterator pos, T&& x){list_node* new_node = new list_node(forward<T>(x));// ... 实现节点插入逻辑,如更新前后节点的指针}// ... 其他成员函数和数据成员省略
};

在上述代码中,list容器模板类实现了对元素的push_back操作,包括接受左值引用和右值引用两种版本。针对右值引用,主要关注void push_back(T&& x)函数:

void push_back(T&& x)
{insert(end(), forward<T>(x));
}
  • 在这个版本中,push_back接受一个右值引用参数T&& x,这意味着它可以高效地处理临时对象或将要被移动的对象。这里的forward<T>(x)调用是为了完美转发参数x,保持其原有的左值或右值属性。

接着,它调用了通用的插入函数insert(iterator pos, T&& x),这里pos是尾部迭代器end()。在插入函数内部:

void insert(iterator pos, T&& x)
{...node* new_node = new node(forward<T>(x));...
}
  • 创建了一个新的list_node对象new_node,并使用forward<T>(x)将参数x传递给list_node的移动构造函数,这样就能有效地将x的数据移动到新节点中,而不是进行拷贝,从而节省了资源,特别是当T类型的对象较大或构造/拷贝成本较高时。

list容器的push_back(T&& x)通过完美转发和移动构造机制,实现了对右值引用参数的高效插入操作,避免了不必要的数据拷贝。

四、新的类功能

1、默认成员函数

在C++标准库中,类的默认成员函数扮演着重要的角色。原先在C++98/03版本中,类默认提供的六个关键成员函数包括:

  1. 默认构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 赋值运算符重载(拷贝赋值操作)
  5. 取址运算符重载(一般不需要显式定义,由编译器自动生成)
  6. 常量成员的取址运算符重载(同样通常由编译器自动生成,与普通取址运算符一致)

其中,前四个函数对于对象的生命周期管理和资源复制至关重要。

随着C++11标准的引入,为了优化资源管理,特别是对于包含动态内存或其他不可复制资源的类,新增了两个默认成员函数:

  1. 移动构造函数
  2. 移动赋值运算符重载

关于移动构造函数和移动赋值运算符的特性及注意事项:

  • 如果开发者没有手动定义移动构造函数,并且也没有定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,编译器会自动生成一个默认的移动构造函数。对于内置类型的数据成员,该函数按照字节逐个进行移动(类似于浅拷贝);而对于自定义类型的数据成员,若该类型自身实现了移动构造函数,则调用其移动构造函数来转移资源,否则退化为调用拷贝构造函数。

  • 同样地,若未定义移动赋值运算符,同时其他相关函数(析构函数、拷贝构造函数或拷贝赋值运算符)也均未定义时,编译器将生成一个默认的移动赋值运算符。在此情况下,内置类型数据成员的处理方式与移动构造函数相同,自定义类型数据成员则视其是否实现了移动赋值运算符而决定采用移动赋值还是拷贝赋值。

  • 当程序员提供了类的移动构造函数或移动赋值运算符时,编译器将不再自动生成对应的拷贝构造函数或拷贝赋值运算符。这意味着在实现高效资源迁移的同时,需要确保类的行为依然满足预期,特别是在涉及深拷贝和资源所有权转移的情况下。

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}
private:bit::string _name;int _age;
};
int main()
{
// 使用默认构造函数创建s1,其_name成员为空字符串,_age为0Person s1; // 对s1进行深拷贝构造s2,调用_string(const string&)构造函数,
//输出:"string(const string& s) -- 深拷贝"Person s2 = s1;// 对s1进行移动构造s3,调用_string(string&&)构造函数,
//输出:"string(string&& s) -- 移动语义",此时s1._name已经被移动,不再是有效的字符串Person s3 = std::move(s1); // 对s2进行移动赋值给s4,调用string& operator=(string&&),
//输出:"string& operator=(string&& s) -- 移动语义",此时s2._name也被移动,不再是有效字符串Person s4;s4 = std::move(s2); 	                        return 0;
}
string(const string& s) -- 深拷贝
string(string&& s) -- 移动语义
string& operator=(string&& s) -- 移动语义

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

在C++11及其后续版本中,程序员对于类的特殊成员函数有了更强的控制权。有时,即使由于用户自定义了一些特殊成员函数,编译器可能不会自动合成某些默认函数。例如,如果显式定义了拷贝构造函数,编译器通常不再生成移动构造函数。这时,若确实需要使用默认实现的移动构造函数,可以使用default关键字显式指定。
class Person
{
public:// 默认构造函数,带有默认参数Person(const char* name = "", int age = 0): _name(name), _age(age){}// 用户自定义拷贝构造函数Person(const Person& p): _name(p._name), _age(p._age){}// 显式要求编译器生成默认的移动构造函数Person(Person&& p) = default;private:bit::string _name; // 假定bit::string是一种支持移动语义的字符串类型int _age;
};int main()
{Person s1;Person s2 = s1; // 使用拷贝构造函数
// 使用移动构造函数,由于已经显式声明为default,所以会调用默认实现Person s3 = std::move(s1);return 0;
}
}

这里,自定义了拷贝构造函数之后,如果没有明确声明移动构造函数,编译器通常不会为其生成移动构造函数(因为已有用户自定义的拷贝构造函数,编译器无法推断移动构造函数是否安全有效)。然而,在许多情况下,尤其是当类的成员变量支持移动语义时,如std::string或假设的bit::string,移动构造函数能够提高效率,通过转移资源而非复制来创建新对象。

通过在移动构造函数声明中使用= default,你可以告诉编译器“尽管我自定义了拷贝构造函数,但我仍希望为移动构造函数提供一个编译器生成的默认实现”。这有助于保持类的移动语义,并在必要时有效地利用资源转移。在上述代码中,s3 = std::move(s1)会调用移动构造函数,将s1中的资源转移到s3中,而不是进行深拷贝。

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

在C++编程语言中,为了禁止编译器自动生成某些默认函数,C++98标准之前的做法通常是将这些函数声明为私有并留空实现,以此阻止其他代码对其进行合法调用。而在C++11及后续标准中,引入了一个更为简洁和直观的机制—delete关键字,它允许开发者明确声明某个函数不应被生成或者应当被视为删除。

class Person
{
public:// 默认构造函数,带有默认参数Person(const char* name = "", int age = 0): _name(name), _age(age){}// 显式删除拷贝构造函数Person(const Person& p) = delete;private:bit::string _name; // 假定bit::string为一种支持移动语义的字符串类型int _age;
};int main()
{Person s1;// 下面这一行尝试使用拷贝构造函数,但由于拷贝构造函数已被删除,所以会导致编译错误// Person s2 = s1;// 移动构造函数未受影响,但如果未显示声明为default或delete,由于拷贝构造函数已自定义,// 编译器也不会生成默认的移动构造函数(除非它能确保安全有效)return 0;
}

在这个例子中,Person(const Person& p) = delete;这一行表明我们希望编译器不要为Person类生成拷贝构造函数,并且任何试图使用拷贝构造函数的地方都会导致编译错误。这样一来,当尝试执行Person s2 = s1;这样的拷贝初始化时,程序将无法通过编译,从而确保了类实例不可被拷贝。

4、emplace

在STL容器中,emplace系列接口如emplace_back相较于传统的insertpush_back方法展现出显著的优势,具体表现为:

template <class... Args>
void emplace_back(Args&&... args);

此接口采用模板可变参数和右值引用(即万能引用)技术,允许直接在容器内使用传入的参数集构造元素,而非先构造临时对象再将其插入容器。

那么相对insert和emplace系列接口的优势到底在哪里呢?
#include <iostream>
#include <list>
#include <utility>// 假设存在一个自定义的bit::string类,这里为了演示只声明不实现
class bit::string {
public:bit::string(const char* str); // 从C风格字符串构造// 其他拷贝构造、移动构造等成员...
};int main() {// 示例一:使用std::pair<int, char>std::list<std::pair<int, char>> simple_list;// 使用emplace_back直接构造pair元素simple_list.emplace_back(10, 'a');simple_list.emplace_back(20, 'b');simple_list.emplace_back(std::make_pair(30, 'c')); // 虽然也可以用,但实际上并不需要make_pairsimple_list.push_back(std::make_pair(40, 'd')); // 这里创建了一个临时pair对象simple_list.push_back({50, 'e'}); // C++11及以后可以直接列表初始化// 输出列表内容for (const auto& e : simple_list) {std::cout << e.first << ":" << e.second << std::endl;}// 示例二:使用std::pair<int, bit::string>std::list<std::pair<int, bit::string>> complex_list;// 使用emplace_back直接构造包含bit::string的pair元素,避免了临时对象的创建complex_list.emplace_back(10, "sort"); complex_list.emplace_back(std::make_pair(20, "sort")); // 同样,这里使用make_pair可以优化// 使用push_back时,会先创建一个临时的pair对象,然后移动到列表中complex_list.push_back(std::make_pair(30, "sort")); complex_list.push_back({40, "sort"}); // 注意:此处假设bit::string支持移动构造,否则push_back可能涉及拷贝构造// 实际输出部分省略,因为bit::string未实现,无法直接输出return 0;
}
  1. 原地构造emplace_back通过接收一组参数并直接在容器尾部构造元素,无需预创建临时对象。在上述示例中,对于std::pair<int, char>类型,虽然表面上看emplace_backpush_back的效果相似,但当处理更复杂的类型时,差异就体现出来了。

    对于std::pair<int, bit::string>的情况,emplace_back接受字符串字面量"sort"作为参数,并直接用于构造bit::string实例,避免了临时bit::string对象的创建和移动构造步骤,从而节省了资源。
     
  2. 完美转发:通过模板参数包和右值引用,emplace_back能够完美地转发参数给元素类型的构造函数,这允许任何类型的参数以最佳方式传递给构造函数,包括右值引用和具有引用绑定的复杂情况。

  3. 高效性:尤其在处理大型对象或具有非默认构造行为的对象(如不可拷贝或移动的对象)时,emplace_back能够减少甚至消除临时对象的生命周期,提高程序运行效率。

  4. 简化和灵活的初始化emplace_back允许用户以更直观的方式初始化容器元素,无需显式创建待插入对象,尤其是在涉及多层嵌套结构或复杂构造逻辑时,这种便利性尤为突出。

总之,在实际编程中,尤其是对性能敏感或者需要动态构造复杂类型元素的情况下,选择使用emplace_back方法不仅能够简化代码,还能提升程序执行效率。

五、可变参数模板

1、概念

在C++11中,一项重大的创新是引入了可变参数模板这一新特性,这使得开发者能够编写能够处理任意数量模板参数的函数模板和类模板。相比于C++98/03标准中仅能处理固定数量模板参数的情况,可变参数模板极大地提升了代码灵活性和复用性,尽管它的概念相对抽象,初次接触时可能显得较为复杂。

下面是一个基础的可变参数模板函数示例:

// Args 是一个模板参数包,而 args 是与之对应的函数实际参数包
// 定义一个参数包 Args... args,它可以收纳从0到任意个数的模板参数。
template <class ...Args>
void ShowList(Args... args)
{// 在此处,args 参数包内可以包含不同类型的任意数量参数// 使用可变参数模板时,直接访问 args 的各个元素并不直接支持索引操作如 args[i]// 而是需要采用参数包展开技术来分别处理每个参数// (此处留空是因为展开的具体实现依赖于具体应用场景)
}
  •  上述ShowList函数模板声明了一个名为Args...的模板参数包,它能够接受任意数量和类型的参数。
  • 参数包args代表传递给函数的实际参数序列。值得注意的是,对于参数包args,我们不能像数组那样通过下标访问其内部元素;
  • 相反,我们必须利用某种机制来“展开”参数包,以便逐个处理其中的每一个参数。这种展开通常借助于编译器提供的折叠表达式或者其他高级技术实现,比如递归模板函数或者std库中的std::initializer_liststd::tuple等工具来进行处理。
  • 对于我们这些初学者而言,掌握如何正确地展开参数包并应用到实际问题是理解和运用可变参数模板的关键所在。随着需求的增长和技术的深入,我们可以逐步探索更多关于可变参数模板的高级用法。

2、递归函数方式展开参数包

递归函数方式在处理可变参数模板时非常常见,可以依次处理参数包中的每一个参数。

首先,定义了一个递归终止函数:

template <class T>
void ShowList(const T& t)
{cout << t << endl;
}
  • 此函数接受一个类型为T的参数t,并将其输出到控制台,然后换行。当参数包只剩下一个参数时,就调用这个函数进行处理。

接下来,定义了一个递归展开函数:

template <class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value << " ";// 调用自身来处理剩余的参数包,通过这种方式递归展开ShowList(args...);
}
  • 此函数接受一个类型为T的参数value,先将其输出到控制台,并在其后加一个空格。然后,通过ShowList(args...)调用自身来处理剩余的参数包,直到参数包为空,此时会调用上述的递归终止函数。

main函数中:

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

分别调用了ShowList函数三次,传入了不同数量和类型的参数。编译器会根据传入的参数自动匹配相应的函数模板进行展开。

  • 第一次调用时,参数包只有一个整数1,递归过程直接调用终止函数,输出1后换行。
  • 第二次调用时,参数包有两个参数,整数1和字符'A',先输出1和空格,然后展开剩余的参数包'A',最终输出1 A后换行。
  • 第三次调用时,参数包有三个参数,整数1、字符'A'和字符串std::string("sort"),同样按照递归展开的过程,依次输出各个参数并用空格隔开,最终输出1 A sort后换行。

3、逗号表达式展开参数包

在C++11中,除了递归函数方式外,还可以通过逗号表达式结合初始化列表来实现可变参数模板的展开。这种方式不需要单独的递归终止函数,而是直接在展开函数内部处理每个参数。

首先,定义一个用于打印单个参数的辅助函数:

template <class T>
void PrintArg(T t)
{cout << t << " ";
}
  • 此函数接受任意类型T的参数t,并将其输出到控制台,后面跟着一个空格。

接着,我们定义了一个使用逗号表达式和初始化列表展开参数包的函数:

template <class ...Args>
void ShowList(Args... args)
{// 初始化列表结合逗号表达式展开参数包// ((PrintArg(args), 0)...) 将会按顺序展开为 ((PrintArg(arg1), 0), (PrintArg(arg2), 0), ..., (PrintArg(argN), 0)) // 其中arg1, arg2, ..., argN分别是Args...参数包中的元素int arr[] = { (PrintArg(args), 0)... }; // 输出换行符,完成参数列表的打印cout << endl;
}
  • ShowList函数中,我们利用C++11的初始化列表和逗号表达式特点创建了一个静态数组arr。逗号表达式(PrintArg(args), 0)首先执行PrintArg(args)打印参数,然后再计算表达式的整体结果(在这里为0)。当初始化数组时,数组元素的构造过程中会依次展开并执行这些逗号表达式,从而间接实现了参数包的展开和打印。

main函数中:

int main()
{ShowList(1);ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}
  • 我们分别调用ShowList函数并传入不同数量和类型的参数。每次调用时,参数包会被展开并在初始化数组arr时逐一调用PrintArg函数进行打印。最后输出换行符结束一行的打印。虽然我们创建了一个数组,但实际上其目的只是为了在初始化的过程中遍历和处理参数包,数组本身的值并无实际意义。

这篇关于C11 列表初始化、左/右值引用、移动语义、可变参数模版的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/878219

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

4B参数秒杀GPT-3.5:MiniCPM 3.0惊艳登场!

​ 面壁智能 在 AI 的世界里,总有那么几个时刻让人惊叹不已。面壁智能推出的 MiniCPM 3.0,这个仅有4B参数的"小钢炮",正在以惊人的实力挑战着 GPT-3.5 这个曾经的AI巨人。 MiniCPM 3.0 MiniCPM 3.0 MiniCPM 3.0 目前的主要功能有: 长上下文功能:原生支持 32k 上下文长度,性能完美。我们引入了

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给

我在移动打工的日志

客户:给我搞一下录音 我:不会。不在服务范围。 客户:是不想吧 我:笑嘻嘻(气笑) 客户:小姑娘明明会,却欺负老人 我:笑嘻嘻 客户:那我交话费 我:手机号 客户:给我搞录音 我:不会。不懂。没搞过。 客户:那我交话费 我:手机号。这是电信的啊!!我这是中国移动!! 客户:我不管,我要充话费,充话费是你们的 我:可是这是移动!!中国移动!! 客户:我这是手机号 我:那又如何,这是移动!你是电信!!

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出 在数字化时代,文本到语音(Text-to-Speech, TTS)技术已成为人机交互的关键桥梁,无论是为视障人士提供辅助阅读,还是为智能助手注入声音的灵魂,TTS 技术都扮演着至关重要的角色。从最初的拼接式方法到参数化技术,再到现今的深度学习解决方案,TTS 技术经历了一段长足的进步。这篇文章将带您穿越时

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而

模版方法模式template method

学习笔记,原文链接 https://refactoringguru.cn/design-patterns/template-method 超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。 上层接口有默认实现的方法和子类需要自己实现的方法