C++面向对象(OOP)编程-C++11新特性详解

        C++11作为一个重要的版本,引入了很多新的特性,解决了C++语言本身很多遗留的内存泄露问题,并且提供了很多比较灵活的用法。引入的auto,智能指针、线程机制都使得C++语言的灵活性、安全性、并发性有了很大的提升。

        本文会比较详细的介绍C++11引入的新特性,以及各自引入的原因和基本的用法。

🎬个人简介:一个全栈工程师的升级之路!
📋个人专栏:C/C++精进之路
🎀CSDN主页 发狂的小花
🌄人生秘诀:学习的本质就是极致重复!

目录

1 C++11 新特性分类

2 C++11新特性详解

2.1 并发编程支持

2.1.1 内存模型

2.1.2 线程和锁

2.1.3 期值

2.2 泛型编程支持

2.2.1 lambda

2.2.2 变参模板

2.2.3 别名

2.2.4 tuple

2.3 简化使用

2.3.1 auto和decltype

2.3.2 范围for

2.3.3 智能指针

2.3.4 统一初始化

2.3.5 nullptr

2.3.6 constexpr

2.3.7 explicit

2.3.8 final和override

2.3.9 右值引用

2.3.10 移动语义

2.3.11 完美转发


1 C++11 新特性分类

        C++11引入的新特性基本上可以分为并发编程支持、泛型编程支持和简化使用三部分。

如下图是详细的分类:

2 C++11新特性详解

2.1 并发编程支持

2.1.1 内存模型

C++11内存模型是一种抽象,它定义了程序中各种变量(如指针、数组、对象等)的访问方式,以及在多线程环境下的并发操作行为。内存模型解决了由于编译器优化、指令重排等原因导致的可见性问题,从而使得程序员可以更好地控制程序的执行顺序,避免出现竞态条件等问题。

内存类型:

C++11内存模型的主要特性包括:

  1. 原子操作:C++11标准对原子操作进行了定义,并引入了一些新的原子类型和函数,以支持无锁编程。原子操作就是对一个内存上变量(或者叫左值)的读取-变更-存储(load-add-store)作为一个整体一次完成。

  2. 同步原语:C++11提供了一些同步原语,如std::mutex、std::condition_variable等,用于实现线程间的同步和互斥。

  3. 内存模型:C++11从各种不同的平台上抽象出了一个软件的内存模型,并以内存顺序进行描述,以使得想进一步挖掘并行系统性能的程序员有足够简单的手段来完成以往只能通过底层编程技术实现的功能。

  4. 禁止重排序:C++11内存模型规定了某些特定的内存操作不能被重排序。

  5. 内存屏障:C++11内存模型引入了内存屏障(memory barrier)的概念,用于控制内存访问的顺序,以避免编译器对代码的重排导致的问题。

  6. 内存序:C++11标准中提供了6种memory order,来描述内存模型。

例子:

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <vector>

std::mutex mtx; // 定义互斥量
std::atomic<int> x(0); // 定义原子变量
std::vector<int> data; // 定义数据容器

void reader() {
    mtx.lock(); // 获取锁
    data.push_back(x.load(std::memory_order_relaxed)); // 读取原子变量的值
    mtx.unlock(); // 释放锁
}

void writer() {
    mtx.lock(); // 获取锁
    x.store(10, std::memory_order_release); // 修改原子变量的值
    mtx.unlock(); // 释放锁
}

int main() {
    std::thread t1(reader); // 创建读线程t1
    std::thread t2(writer); // 创建写线程t2
    t1.join(); // 等待读线程结束
    t2.join(); // 等待写线程结束
    return 0;
}

2.1.2 线程和锁

        C++11还提供了一些同步机制,例如互斥锁、条件变量、自旋锁、读写锁等。其中,条件变量是一种同步原语,用于阻塞一个或多个线程,直到接收到另一个线程的通知信号,或暂停信号,或伪唤醒信号。自旋锁是一种忙等待锁,它在等待锁的过程中不会使线程进入睡眠状态。读写锁允许多个线程同时读取共享资源,但在写入时只允许一个线程访问。


• mutex——系统的互斥锁,支持 lock()、unlock() 和保证 unlock() 的 RAII 方式
• condition_variable——系统中线程间进行事件通信的条件变量
• thread_local——线程本地存储

线程和锁模型需要使用某种形式的同步来避免竞争条件。C++11 为此提供了标准 的 mutex(互斥锁)。

2.1.3 期值

        在C++11中,期值(future)是一种用于异步计算的对象,它代表了一个尚未完成但将来会得到的值。期值的主要用途是实现并发编程中的异步操作,例如在一个线程中执行某个耗时的操作,然后在另一个线程中等待该操作的结果。

C++11主要提供的API

future——一个句柄,通过它你可以从一个共享的单对象缓冲区中 get() 一个值,可能需要等待某个 promise 将该值放入缓冲区。

• promise——一个句柄,通过它你可以将一个值 put() 到一个共享的单对象缓冲区,可能会唤醒某个等待 future 的 thread。

• packaged_task——一个类,它使得设置一个函数在线程上异步执行变得容易,由 future 来接受 promise 返回的结果。

• async()——一个函数,可以启动一个任务并在另一个 thread 上执行。

可以通过std::future来获取期值,其基本用法如下:

#include <iostream>
#include <thread>
#include <future>

int main() {
    // 创建一个异步任务
    std::future<int> fut = std::async(std::launch::async, [](){
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
        return 42; // 返回结果
    });

    // 在另一个线程中等待结果
    std::cout << "Result: " << fut.get() << std::endl; // 输出结果
    return 0;
}

一个例子:

#include <iostream>
#include <future>

int main() {
    std::future<int> result;

    // 启动一个异步任务来计算两个数之和
    result = std::async(std::launch::async, [](int a, int b) {
        return a + b;
    }, 3, 4);

    // 在主线程中等待异步任务完成并获取结果
    int sum = result.get();
    std::cout << "两数之和为:" << sum << std::endl;

    return 0;
}

2.2 泛型编程支持

2.2.1 lambda

C++11引入了lambda表达式,它是一种匿名函数,可以在代码中直接定义和使用。lambda表达式的语法如下:

[capture](parameters) -> return_type { function_body }

其中:

  • capture:捕获列表,用于捕获外部变量。可以有值捕获(按值传递)和引用捕获(按引用传递)。
  • parameters:参数列表,与普通函数的参数列表类似。
  • return_type:返回类型,可以省略,编译器会自动推导。
  • function_body:函数体,包含实现的代码。

下面是一个简单的lambda表达式示例:

#include <iostream>

int main() {
    auto add = [](int a, int b) -> int { return a + b; };
    int sum = add(3, 4);
    std::cout << "Sum: " << sum << std::endl;
    return 0;
}

捕获列表的几种形式:

1.不捕获任何外部变量:

[]{ /* code */ }

2.按值捕获所有外部变量:

[=]{ /* code */ }

3.按引用捕获所有外部变量:

[&]{ /* code */ }

4.按值捕获特定变量:

[x]{ /* code */ }

5.按引用捕获特定变量:

[&x]{ /* code */ }

6.混合捕获模式:

[x, &y]{ /* code */ }

7.隐式捕获某些变量,显式捕获其他变量:

[=, &x]{ /* code */ }
[&x, y]{ /* code */ }

例子:
1.不捕获任何外部变量:

[]{ return 23; }

一个简单的 lambda,不使用任何外部变量,返回 23。

2.按值捕获外部变量:

int x = 2;
auto l = [=]{ return x + 1; };

这个 lambda 按值捕获 x,返回 x + 1 的结果。

3.按引用捕获外部变量并修改:

int x = 1;
auto l = [&x]{ ++x; };

按引用捕获 x 并在 lambda 内部对其进行修改。

4.具有参数的 lambda:

auto l = [](int a, int b){ return a + b; };

一个接受两个整数参数并返回它们的和的 lambda。

5.可变 lambda(修改按值捕获的变量):

int x = 1;
auto l = [x]() mutable { return ++x; };

mutable 关键字允许 lambda 修改按值捕获的变量。

6.带有显式返回类型的 lambda:

auto l = [](int a, int b) -> double { return (a + b) / 2.0; };

一个计算平均值的 lambda,显式指定返回类型为 double。
 

2.2.2 变参模板

C++11中的变参模板允许函数或类模板接受可变数量的参数。在函数模板中,可以使用省略号(...)表示可变数量的参数;在类模板中,可以使用参数包(parameter pack)表示可变数量的类型参数。

下面分别举一个变参函数模板和一个变参类模板的例子:

  • 变参函数模板
template<typename T, typename... Args>
void foo(T t, Args... args) {
    // ...
}

在上面的例子中,foo函数模板可以接受任意数量的参数,其中第一个参数的类型为T,后面的参数类型为Args。这些参数可以是任何类型,包括基本类型、类类型和其它模板类型。

在函数体内,可以使用递归展开(recursive expansion)来访问这些参数。例如:

template<typename T, typename... Args>
void bar(T t, Args... args) {
    foo(t); // 处理第一个参数
    bar(args...); // 递归调用自身,处理剩余参数
}

在上面的例子中,bar函数首先调用foo函数处理第一个参数,然后使用递归展开的方式调用自身来处理剩余的参数。这样,我们就可以实现类似于printf函数的功能,接受任意数量的参数并进行处理。

  • 变参类模板
template<typename T, typename... Args>
class MyClass {
public:
    MyClass(T t, Args... args) {
        // ...
    }
};

在上面的例子中,MyClass类模板可以接受任意数量的类型参数。在构造函数中,可以使用参数包展开(parameter pack expansion)来初始化成员变量。例如:

template<typename T, typename... Args>
void bar(T t, Args... args) {
    MyClass<T, Args...> obj(t, args...); // 创建对象并初始化成员变量
}

在上面的例子中,我们使用MyClass<T, Args...>来创建一个对象,并将传入的参数传递给构造函数进行初始化。这样,我们就可以实现类似于printf函数的功能,接受任意数量的参数并进行处理。

一个简单的例子:

#include <iostream>
#include <string>
#include <sstream>

template<typename T>
std::string toString(const T& value) {
    std::ostringstream oss;
    oss << value;
    return oss.str();
}

template<typename T, typename... Args>
std::string toString(const T& value, const Args&... args) {
    std::ostringstream oss;
    oss << value;
    return oss.str() + " " + toString(args...);
}

template<typename... Args>
void myPrintf(const char* format, const Args&... args) {
    std::cout << toString(format, args...) << std::endl;
}

int main() {
    myPrintf( "Alice", 30,23.8);
    return 0;
}

        运行结果:

Alice 30 23.8

2.2.3 别名

在C++11中,引入了类型别名(Type Alias)的新特性,允许开发者为现有的数据类型创建一个新的名称。这个功能有助于提高代码的可读性、可维护性和可重用性。类型别名可以通过typedef关键字或using关键字来定义。

以下两种typedef和using的使用方式:

// 别名

typedef int myInt;
myInt kp;

using IntVector = std::vector<int>; 
IntVector ki;
ki.push_back(23);
for (auto i : ki)
{
    std::cout << i << std::endl;
}

using MYint = int;
MYint lo = 4;
std::cout << lo << std::endl;

这两种方法在语义上是等效的。另外,值得一提的是,使用using关键字定义的模板别名相比typedef更具优势,它更简洁,并且可以完全像新的类模板一样使用。

2.2.4 tuple

C++11中的tuple是一个模板类,它可以存储任意数量和类型的数据。它的主要优点是可以方便地访问元组中的元素,并且支持元组的复制、移动和比较等操作。

使用tuple的基本语法如下:

std::tuple<type1, type2, ...> tuple_name(value1, value2, ...);

其中,type1、type2等是元组中元素的类型,value1、value2等是对应的元素值。

例如,我们可以定义一个包含两个int类型和一个string类型的tuple:

#include <iostream>
#include <tuple>
#include <string>

int main() {
    std::tuple<int, int, std::string> myTuple(1, 2, "hello");
    return 0;
}

要访问tuple中的元素,可以使用std::get函数。例如,如果我们想访问上述定义的tuple中的第一个元素(一个int),可以使用std::get<0>(myTuple)来获取。需要注意的是,元组的索引是从0开始的。

此外,tuple还支持一些其他操作,例如创建元组的副本、移动元组等。例如,我们可以使用std::make_tuple函数来创建一个包含两个int类型和一个string类型的tuple:

std::tuple<int, int, std::string> anotherTuple = std::make_tuple(3, 4, "world");

我们还可以使用std::tie函数将tuple中的元素解包到多个变量中:

int a, b;
std::string c;
std::tie(a, b, c) = myTuple;

最后,我们可以使用std::tuple_cat函数将两个或多个tuple连接起来:

std::tuple<int, int, std::string> combinedTuple = std::tuple_cat(myTuple, anotherTuple);

结构化绑定:

// tuple
std::tuple<int, int, std::string> myTuple(1, 2, "hello");

auto [value1, value2, value3] = myTuple;
// 结构化绑定
std::cout << value1 << " " << value2 << " " << value3 << " " << std::endl;

2.3 简化使用

2.3.1 auto和decltype

C++11中用auto关键字来支持自动类型推导。用decltype推导表达式类型。头文件:#include<typeinfo>

auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型。(前提是定义一个变量时对其进行初始化)

auto a = 10; // 10是int型,可以自动推导出a是int

decltype:用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。(decltypde是不需要推导变量初始化的,根据的是表达式对变量的类型就可以推导。)

auto varname = value;
decltype(exp) varname = value;
decltype(10.8) x;  //x 被推导成了 double

两者区别:

1,auto用于变量的类型推导,根据初始化表达式的类型来推导变量的类型,常用于简化代码和处理复杂类型。而decltype则用于获取表达式的类型,保留修饰符,并且可以进行表达式求值。

2,auto在初始化时进行类型推导,而decltype直接查询表达式的类型,可以用于任何表达式,包括没有初始化的变量。

3,auto在编译期间确定类型,并且无法更改。而decltype在运行时才确定表达式的类型。

4,auto适用于简单的类型推导,而decltype适用于复杂的类型推导和获取表达式的结果类型。

2.3.2 范围for

范围for是C++11中引入的一种新的循环结构,用于遍历容器或数组中的元素。它的基本语法如下:

for (declaration : range) {
    // 执行语句
}
其中,declaration表示声明一个变量来存储当前迭代的值,range表示要遍历的范围,可以是容器、数组、字符串等。

例如,遍历一个vector中的元素可以使用以下代码:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> myVector = {1, 2, 3, 4, 5};
    for (int num : myVector) {
        std::cout << num << std::endl;
    }
    for (auto num : myVector) {
            std::cout << num << std::endl;
        }


    return 0;
}

在这个例子中,我们使用了一个int类型的变量num来存储当前迭代的值,然后通过std::cout输出到控制台。每次循环时,num会自动更新为myVector中的下一个元素。当myVector中的所有元素都被遍历完后,循环结束。这里也可以使用auto来推导。

2.3.3 智能指针

在C++11中,引入了新的智能指针类,用于更安全和方便地管理动态分配的资源,避免内存泄漏和悬空指针等问题。以下是C++11中的三种主要智能指针:

std::unique_ptr

1,std::unique_ptr 是一种独占式智能指针,用于管理唯一的对象,确保只有一个指针可以访问该对象。

2,使用 std::unique_ptr 可以自动释放动态分配的内存,当指针超出作用域或被重置时,它会自动删除所管理的对象。

3,通过 std::make_unique 函数可以创建 std::unique_ptr 对象,如:std::unique_ptr<int> ptr = std::make_unique<int>(42);

std: :shared_ptr:

1,std::shared_ptr 是一种共享式智能指针,多个指针可以同时共享对同一对象的拥有权。

2,std::shared_ptr 使用引用计数技术追踪所管理对象的引用数量,当引用计数变为零时,自动销毁所管理的对象。

3,通过 std::make_shared 函数可以创建 std::shared_ptr 对象,如:std::shared_ptr<int> ptr = std::make_shared<int>(42);

std::weak_ptr

1,std::weak_ptr 是一种弱引用智能指针,它可以解决 std::shared_ptr 的循环引用问题。

2,std::weak_ptr 指向 std::shared_ptr 管理的对象,但不会增加引用计数。因此,当所有 std::shared_ptr 对象超出作用域后,即使还有 std::weak_ptr 对象存在,所管理的对象也会被销毁。

3,通过 std::shared_ptr 的 std::weak_ptr 构造函数可以创建 std::weak_ptr 对象,如:std::weak_ptr<int> weakPtr = sharedPtr;

2.3.4 统一初始化

C++11引入了统一初始化(Uniform Initialization)的概念,它允许在构造函数参数列表中使用花括号{}来初始化对象。这种初始化方式可以用于创建数组、结构体、类等对象。

例如,对于数组的初始化:

int arr[] = {1, 2, 3}; // 使用花括号{}初始化数组
对于结构体的初始化:
struct MyStruct {
    int a;
    double b;
    char c;
};

MyStruct ms = {1, 2.0, 'c'}; // 使用花括号{}初始化结构体

对于类的初始化:

class MyClass {
public:
    int a;
    double b;
    char c;
};

MyClass mc = {1, 2.0, 'c'}; // 使用花括号{}初始化类

统一初始化还可以用于lambda表达式和捕获变量的初始化:

auto func = [](int x, int y) -> int { return x + y; }; // 使用花括号{}初始化lambda表达式
int a = 10;
int b = 20;
[a, b]() mutable { // 使用花括号{}初始化捕获变量
    std::cout << "a: " << a << ", b: " << b << std::endl;
}();

2.3.5 nullptr

C++11引入了一个新的关键字nullptr,用于表示空指针。它与NULL和0等传统空指针表示方法不同,是一个类型安全的空指针常量。

nullptr的类型是std::nullptr_t,它是一个特殊的整数类型,用于表示空指针。在C++11中,可以使用nullptr来初始化或赋值给任何指针类型,包括原生指针、智能指针和成员指针等。

int* ptr = nullptr; // 使用nullptr初始化原生指针
std::shared_ptr<int> sp = nullptr; // 使用nullptr初始化智能指针
MyClass obj;
obj.setPointer(nullptr); // 使用nullptr赋值给成员指针

需要注意的是,在使用nullptr时,应该尽量避免将其与NULL或0混淆。因为NULL和0在某些情况下可能会被编译器识别为整型字面量,从而导致意外的结果。

2.3.6 constexpr

constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。

常量表达式(const experssion)是指:

(1)值不会改变

(2)在编译过程就能得到计算结果的表达式。

constexpr和const的区别:

两者都代表可读,

const只表示read only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量,

而constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量,但是如果编译期间此函数不能被计算出来,那它就会当作一个普通函数被处理。

#include<iostream>
using namespace std;

constexpr int func(int i) {
    return i + 1;
}

int main() {
    int i = 2;
    func(i);// 普通函数
    func(2);// 编译期间就会被计算出来
}

2.3.7 explicit

在C++11中,explicit关键字用于修饰类的构造函数,表示该构造函数是显式的,不能进行隐式类型转换。

class MyClass {
public:
    explicit MyClass(int a) {} // 显式构造函数
};

// MyClass obj = 10; // 错误,不能进行隐式类型转换
MyClass obj2(10); // 正确,需要显示调用构造函数

使用explicit关键字可以防止因编译器自动进行类型转换而导致的错误或不必要的代码生成。同时,它也可以强制程序员显式地指定参数类型,提高代码的可读性和安全性。

2.3.8 final和override

在C++11中,final和override是两个关键字,用于修饰类的成员函数。

final关键字表示该成员函数不能被继承或覆盖。当一个基类的虚函数被声明为final时,派生类不能再重写该函数。这可以防止派生类无意中修改基类的行为。

override关键字用于显式地指示派生类中的虚函数是覆盖基类中的虚函数。如果没有使用override关键字,编译器可能会因为函数签名不匹配而报错。使用override关键字可以避免这种情况的发生,提高代码的可读性和安全性。

class Base {
public:
    virtual void func() final {} // 基类中的虚函数被声明为final,不能被覆盖
};

class Derived : public Base {
public:
    void func() override {} // 派生类中的虚函数覆盖了基类中的虚函数,并使用了override关键字
};

2.3.9 右值引用

C++ 中的右值引用(Rvalue reference)是一种引用类型,它用于绑定到临时对象或将要被移动的对象(右值)。通过右值引用,我们可以对右值进行有效的操作,如移动语义和完美转发。

右值引用的语法是在类型后面加上 &&,例如 int&& 表示一个右值引用到 int 类型的对象。右值引用只能绑定到右值,不能绑定到左值。

右值引用主要有两个重要的应用场景:移动语义和完美转发。

【1】移动语义: 右值引用使得我们可以实现高效的资源管理,尤其是在处理动态分配的内存或大型对象时。通过移动语义,我们可以将资源从一个对象转移到另一个对象,避免了不必要的拷贝开销。
通过定义移动构造函数和移动赋值运算符,并使用右值引用参数,可以实现对资源的高效转移。移动构造函数用于在构造对象时从临时或将要被销毁的对象中“窃取”资源,移动赋值运算符用于在对象已存在时将资源从右值赋值给对象。这样,在资源转移完成后,原始对象就不再拥有该资源,而新对象拥有该资源,避免了多余的内存分配和拷贝操作。

【2】完美转发: 完美转发是指在函数模板中保持参数的值类别(左值或右值)并将其转发到其他函数,以实现泛型编程中的通用参数传递。通过使用右值引用和模板参数推导,可以实现参数类型的自动推导和类型保持。
在函数模板中使用右值引用参数可以接收右值和左值,并保持参数的原始类型。结合 std::forward 可以实现完美转发,将参数以原始类型转发到其他函数。这样,在调用模板函数时,参数的值类别被保留,从而选择正确的函数进行处理。

右值引用在 C++11 中引入,它的出现在很大程度上优化了资源管理和提升了代码的性能。它为移动语义和完美转发提供了重要的基础,并在现代 C++ 开发中广泛应用。

2.3.10 移动语义

C++11 引入了移动语义(Move Semantics)的概念,旨在提高对象的性能和效率。移动语义通过转移资源所有权,避免不必要的拷贝操作,从而更高效地管理对象。

在传统的拷贝语义中,当我们将一个对象赋值给另一个对象或者作为函数参数传递时,会进行对象的拷贝操作,这包括复制所有成员变量的值、分配内存等。在某些情况下,这种拷贝操作是非常昂贵的,特别是对于大型对象或者资源密集型的操作。

移动语义通过引入右值引用(Rvalue Reference)来解决这个问题。右值引用使用 && 语法进行声明,表示一个临时对象或者即将销毁的对象。在移动语义中,我们可以将资源的所有权从一个对象转移到另一个对象,而不需要进行昂贵的拷贝操作。

在使用移动语义时,可以借助 std::move 函数将左值转换为右值引用,以便进行移动操作。下面是一个简单的示例:

#include <iostream>
#include <vector>

class Person {
public:
    Person() {
        std::cout << "默认构造函数" << std::endl;
        // 假设需要分配大量内存或进行其他资源密集型操作
    }

    Person(const Person& other) {
        std::cout << "拷贝构造函数" << std::endl;
        // 实现对象的拷贝操作
    }

    Person(Person&& other) {
        std::cout << "移动语义构造函数" << std::endl;
        // 实现对象的移动操作
    }
};

int main() {
    Person person1;  // 调用默认构造函数

    Person person2(person1);  // 调用拷贝构造函数,拷贝 person1 的值到 person2
    Person person3(std::move(person1));  // 调用移动构造函数,将 person1 的值转移到 person3

    return 0;
}

通过移动语义,我们可以避免不必要的拷贝操作,提高代码的性能和效率。特别是对于容器类(如 std::vectorstd::string)或动态分配的资源,利用移动语义可以显著降低内存分配和复制的开销。

需要注意的是,移动构造函数的实现通常是将源对象指针设置为 nullptr,以确保在析构时不会释放已经被转移的资源。此外,移动构造函数和拷贝构造函数应该遵循特定的语义规范,以确保正确、可预期的行为。

2.3.11 完美转发

完美转发(perfect forwarding)是 C++ 中用于保持传递参数类型和转发函数调用的机制。它通常与模板和右值引用一起使用,以实现泛型编程中的参数传递。

在传统的函数调用中,如果我们想要将一个函数的参数传递给另一个函数,通常可以直接通过值传递或引用传递来实现。但是问题在于,当我们希望将参数以原始类型(值类型或引用类型)传递给另一个函数时,需要显式指定参数的类型,而无法自动推导。

完美转发解决了这个问题,它允许我们在一层函数中接收参数,并将其转发到另一层函数,同时保持参数的原始类型。这样就可以实现泛型编程中的通用参数传递,不再需要手动指定参数类型。

完美转发的核心是使用了两种类型:通用引用和 std::forward

  1. 通用引用(Universal Reference)是指使用了 auto&& 或模板参数推导结合引用折叠规则的引用类型。通用引用可以绑定到左值或右值,并保持参数的原始类型。
  2. std::forward 是一个条件转发的工具函数,用于根据参数的原始类型,选择性地将参数转发为左值引用或右值引用。它的使用场景通常是在模板函数或模板类中,用于将参数转发到另一个函数。

一个例子:

#include <iostream>
#include "common.h"
 
using namespace std;
 
 
template<typename F ,typename T, typename U>
void testFun(F f, T && t1, U && t2)
{
    f(std::forward<T>(t1),std::forward<U>(t2));
}
 
void gu_y(int && t1, int & t2) // 接收左值和右值
{
    cout << t1+t2 << endl;
}
 
int main(int argc, char *argv[])
{
    {
        __LOG__("完美转发");
        int per1 = 23;
        int per2 = 34;
 
        cout << "左右: ";
        testFun(gu_y,23,per2);// 传入右值和左值
    }
 
    return 0;
}

        运行结果:

对于T && t1 ,t1始终是变量是左值,因此在传入函数gu_y()时,需要将一个左值转换成右值,又由于这是一个模板函数,因此不可以使用移动语义将其直接固定为右值,所以提出了完美转发。

🌈我的分享也就到此结束啦🌈
如果我的分享也能对你有帮助,那就太好了!
若有不足,还请大家多多指正,我们一起学习交流!
📢未来的富豪们:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!最后,☺祝愿大家每天有钱赚!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ffmpeg两种windows版本区别说明

版本一 必须拷贝exe和dll文件才能使用&#xff0c;如果缺少dll则exe不正正常执行 如果缺少dll &#xff0c;执行 exe会报错如下 版本2 直接拷贝exe就能使用&#xff0c;没有依赖的环境

防火墙什么用,软件防火墙与硬件防火墙有什么不一样

防火墙是一种网络安全技术&#xff0c;通过有机结合各类用于安全管理与筛选的软件和硬件设备&#xff0c;在计算机网络的内、外网之间构建一道相对隔绝的保护屏障&#xff0c;以保护用户资料与信息的安全性。 防火墙的作用的详细说明&#xff1a; 1.访问控制&#xff1a;防火…

绝地求生:【PC】12月额外特殊空投

亲爱的玩家朋友们&#xff0c;大家好&#xff01; 有一个好消息要告诉大家&#xff0c;12月丰富的活动列表中又添加了新的活动啦&#xff01;希望大家在闲游盒PUBG的陪伴下&#xff0c;为2023年画上圆满的句号&#xff01; ※可在大厅内的活动页面查看活动详情 PUBG空投节&am…

[每周一更]-(第44期):GIT版本控制之忽略文件

基础概念 在 Git 中&#xff0c;可以通过 .gitignore 文件来指定不需要纳入版本控制的文件或文件夹&#xff0c;这些被忽略的文件或文件夹不会被提交到仓库中。 在项目根目录下创建一个名为 .gitignore 的文件&#xff0c;并在其中列出需要忽略的文件或文件夹。一些常见的示例…

Spring Boot学习随笔- Jasypt加密数据库用户名和密码以及解密

学习视频&#xff1a;【编程不良人】2021年SpringBoot最新最全教程 第十九章、Jasypt加密 Jasypt全称是Java Simplified Encryption&#xff0c;是一个开源项目。 Jasypt与Spring Boot集成&#xff0c;以便在应用程序的属性文件中加密敏感信息&#xff0c;然后在应用程序运行…

如何在VSCode搭建ESP-IDF开发ESP32

文章目录 概要安装VScode安装ESP-IDF插件使用官方例程小结 概要 ESP-IDF(Espressif IoT Development Framework) 即乐鑫物联网开发框架&#xff0c;它基于 C/C 语言提供了一个自给自足的 SDK&#xff0c;可为在 Windows、Linux 和 macOS 系统平台上开发 ESP32 应用程序提供工具…

架构设计系列 5:常见架构介绍

前面讲了架构是什么&#xff0c;架构的发展史&#xff0c;架构设计的基础理论&#xff0c;这次针对常见架构设计风格进行介绍和分析。 一、MVC&#xff1a;三层架构经典 经典的 MVC 架构&#xff08;Model-View-Controller&#xff09;架构是软件系统架构设计中的经典&#xf…

2022年第十三届中国数据库技术大会(DTCC2022)-核心PPT资料下载

一、峰会简介 本届大会以“数据智能 价值创新”为主题&#xff0c;设置2大主会场&#xff0c;20技术专场&#xff0c;邀请超百位行业专家&#xff0c;重点围绕时序数据库、图数据技术、实时数仓技术与应用实践、云原生数据库、大数据平台与数据安全等内容展开分享和探讨&#…

18-网络安全框架及模型-信息系统安全保障模型

信息系统安全保障模型 1 基本概念 信息系统安全保障是针对信息系统在运行环境中所面临的各种风险&#xff0c;制定信息系统安全保障策略&#xff0c;设计并实现信息系统安全保障架构或模型&#xff0c;采取工程、技术、管理等安全保障要素&#xff0c;将风险减少至预定可接受的…

小梅哥Xilinx FPGA学习笔记18——专用时钟电路 PLL与时钟向导 IP

目录 一&#xff1a;IP核简介&#xff08;具体可参考野火FPGA文档&#xff09; 二&#xff1a; 章节导读 三&#xff1a;PLL电路原理 3.1 PLL基本实现框图 3.2 PLL倍频实现 3.3 PLL分频实现 四: 基于 PLL 的多时钟 LED 驱动设计 4.1 配置 Clocking Wizard 核 4.2 led …

[卷积神经网络]FCOS--仅使用卷积的Anchor Free目标检测

项目源码&#xff1a; FCOShttps://github.com/tianzhi0549/FCOS/ 一、概述 作为一种Anchor Free的目标检测网络&#xff0c;FCOS并不依赖锚框&#xff0c;这点类似于YOLOx和CenterNet&#xff0c;但CenterNet的思路是寻找目标的中心点&#xff0c;而FCOS则是寻找每个像素点&…

[每周一更]-(第43期):Golang版本的升级历程

从1.13接触go语言开始更新我们公司内第一个Go项目&#xff0c;直至现在go版本已经发展到1.20&#xff08;20230428&#xff09;&#xff0c;我们从go发版开始认识go语言&#xff0c;有利于我们更深入 了解这门语言&#xff0c;洞悉一些深层方式&#xff0c;加深我们学习的动力&…

Android---Kotlin 学习013

互操作性和可空性 Java 世界里所有对象都可能是 null&#xff0c;而 kotlin 里面不能随便给一个变量赋空值的。所有&#xff0c;kotlin 取调用 java 的代码就很容易出现返回一个 null&#xff0c;而 Kotlin 的接收对象不能为空&#xff0c;你不能想当然地认为 java 的返回值就…

有了向量数据库,我们还需 SQL 数据库吗?

“除了向量数据库外&#xff0c;我是否还需要一个普通的 SQL 数据库&#xff1f;” 这是我们经常被问到的一个问题。如果除了向量数据以外&#xff0c;用户还有其他标量数据信息&#xff0c;那么其业务可能需要在进行语义相似性搜索前先根据某种条件过滤数据&#xff0c;例如&a…

数据仓库-数仓优化小厂实践

一、背景 由于公司规模较小&#xff0c;大数据相关没有实现平台化&#xff0c;相关的架构都是原生的Apache组件&#xff0c;所以集群的维护和优化都需要人工的参与。根据自己的实践整理一些数仓相关的优化。 二、优化 1、简易架构图 2、ODS层优化 2.1 分段式解析 随着业务增长…

GoLang学习之路,对Elasticsearch的使用,一文足以(包括泛型使用思想)(二)

书写上回&#xff0c;上回讲到&#xff0c;Elasticsearch的使用前提即&#xff1a;语法&#xff0c;表结构&#xff0c;使用类型结构等。要学这个必须要看前面这个&#xff1a;GoLang学习之路&#xff0c;对Elasticsearch的使用&#xff0c;一文足以&#xff08;包括泛型使用思…

spring、springmvc、springboot、springcloud简介

spring简介 spring是什么&#xff1f; spring: 春天spring: 轻量级的控制反转和面向切面编程的框架 历史 2002年&#xff0c;首次推出spring雏形&#xff0c;interface 21框架2004年&#xff0c;发布1.0版本Rod Johnson: 创始人&#xff0c;悉尼大学&#xff0c;音乐学博士…

SeaTunnel同步PostgreSQL数据至ClickHouse(1)

ClickHouse简介 ClickHouse最初是为Yandex.Metrica世界第二大Web分析平台而开发的。多年来一直作为该系统的核心组件被该系统持续使用着。目前为止&#xff0c;该系统在ClickHouse中有超过13万亿条记录&#xff0c;并且每天超过200多亿个事件被处理。它允许直接从原始数据中动…

【MATLAB】交叉验证求光滑因子的广义神经网络时序预测算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 交叉验证求光滑因子的广义神经网络时序预测算法的基本原理如下&#xff1a; 首先&#xff0c;我们需要了解什么是交叉验证和光滑因子。交叉验证是一种评估模型性能的常用方法&#xff0c…

Spring高手之路-Spring事务的传播机制(行为、特性)

目录 含义 七种事务传播机制 1.REQUIRED&#xff08;默认&#xff09; 2.REQUIRES_NEW 3.SUPPORTS 4.NOT_SUPPORTED 5.MANDATORY 6.NEVER 7.NESTED 含义 事务的传播特性指的是当一个事务方法被另一个事务方法调用时&#xff0c;这个事务方法应该如何进行&#xff1f; 七…