c++新特性左右值引用和引用折叠,完美转发

最近在回顾cpp新特性,对左右值进行一个总结。

理论

C++中的左值引用(lvalue reference)和右值引用(rvalue reference)是两个非常重要的概念,它们对于理解C++的类型系统和编程模式有着至关重要的作用。

左值引用(lvalue reference):

  • 左值(lvalue)是表示可以取地址的表达式,可以出现在赋值语句的左边。
  • 左值引用使用T&的形式声明,比如int& lref = x; 变量x可以取地址

右值引用(rvalue reference):

  • 右值(rvalue)是不能出现在赋值语句左边的表达式。它代表一个临时的、无法直接访问的对象。

  • 右值引用是一个指向右值的引用。它允许你以更高效的方式操作这些临时对象。

  • 右值引用使用T&&的形式声明,比如int&& rref = 20;

左值引用和右值引用的主要区别在于:

  • 左值引用绑定到一个持久的对象,可以对其进行修改。

  • 右值引用绑定到一个临时的对象,可以用来优化对这些临时对象的操作。

右值引用的引入带来了许多好处,比如:

  • 移动语义(move semantics)可以减少不必要的拷贝,提高性能。

  • 完美转发(perfect forwarding)可以保留函数参数的引用类型。

  • 可以实现更高效的容器管理和内存分配。

进阶理解

C++左值、右值、prvalue、xvalue和glvalue等概念

  1. 在旧的C++标准(C++11之前)中,右值指的是临时对象或字面量(整形42,字符‘c’等),而左值指的是可以被取地址的表达式。

  2. 在新的C++标准(C++11及之后)中,右值被细分为两种:

    • prvalue(纯右值):字面量、需要计算的表达式结果等,不能被取地址
    • xvalue(消亡值):通过将一个对象绑定到右值引用上而产生的表达式,可以被取地址。
    • 
      123这种就是纯右值
      
      int arr[] = {1, 2, 3, 4};
      int&& x = arr[2]; //当数组下标运算符的操作数是一个数组右值时,返回的表达式就是一个消亡值。
      
      std::string s1 = "hello";
      std::string s2 = std::move(s1); //返回的是一个将 s1 绑定到右值引用的表达式,是一个消亡值(xvalue)。这样可以触发移动语义,避免不必要的拷贝。
      
      int x = 10, y = 20;
      int&& z = (x > y) ? x : y; // 返回的表达式是一个消亡值,可以绑定到右值引用。
      
      总结一下: 语义结合后(抽象理解)是右值,并且会返回出来。 
      比如:arr[2] 结合后返回是右值   std::move 结合后返回是右值  (x > y) ? x : y结合返回是右值
      

  3. 泛左值(glvalue)是一个更广泛的概念,包括传统意义上的左值和xvalue。所有可被取地址的表达式都是glvalue。

  4. 左值表达式的特点: 有合法的地址

    • 变量名、函数名、一些复合类型(如数组、指针等)下标和成员访问表达式
    • 前置自增/自减运算符、解引用运算符
    • 可以作为赋值运算符的左操作数
    • 可以被用于初始化左值引用
  5. 右值表达式的特点: 没合法的地址

    • 不能被取地址,因为可能没有。 
    • 不能作为赋值运算符的左操作数,因为右值不能直接访问。
    • 可以用于初始化右值引用,延长临时对象的生命周期
  6. 等号左边的表达式不一定是左值,因为C++允许重载赋值运算符,此时等号左边可能是一个自定义的右值类型。

回归实践

基础:
int x = 2;  
int是变量类型 x是变量  即x是一个int类型的变量

int & b = x;
int是变量类型 b是变量 &是什么?  &是左值引用声明符号。 即b是一个整型左值引用变量

int && a = 2;
int是变量类型 a是变量 &&是什么?  &&是右值引用声明符号。 即a是一个整型右值引用变量


进阶:
int x = 10;      // x is an lvalue
int y = x + 5;   // x + 5 is an rvalue

int&& r1 = 42;   // 42 is an rvalue, r1 is an rvalue reference
int& r2 = x;     // x is an lvalue, r2 is an lvalue reference

std::string s1 = "hello";
std::string s2 = s1;        // s1 is an lvalue, copy constructor is called
std::string s3 = std::move(s1); // s1 is an xvalue, move constructor is called

int arr[] = {1, 2, 3, 4};
int&& x = arr[2];           // arr[2] is an xvalue

struct Foo { int value; };
Foo foo;
Foo&& bar = std::move(foo).value; // foo.value is an xvalue

void func() {}
int&& z = (10 > 20) ? 10 : 20; // (10 > 20) ? 10 : 20 is an xvalue
int&& w = func(); // func() is a prvalue, but cannot be used to initialize a reference

技能:移动语义

前行提要

搬移构造:把石头从a搬移到b,这个过程就是搬移构造,最终石头只有一份。拥有者变了 a不再拥有。

拷贝构造:a把石头拷贝一份b,这个过程就是拷贝构造,最终石头有2份。ab都有实现共同富裕。

经典深拷贝和浅拷贝问题,因为类内有指针管理内存,使用编译器默认构造,会造成指针移动但是内存仅有一份(拷贝构造成了搬移构造),所以类内有指针一定要重写拷贝和搬移构造!!!

一句话总结就是移动语义std::move包过MyString(MyString&& other) 搬移构造

#include <iostream>
#include <string>

class MyString {
public:
    MyString() : m_data(nullptr) {}
    MyString(const char* str) : m_data(new char[strlen(str) + 1]) {
        strcpy(m_data, str);
    }
//拷贝构造
    MyString(const MyString& other) : m_data(new char[strlen(other.m_data) + 1]) {
        strcpy(m_data, other.m_data);
    }
//搬移构造
    MyString(MyString&& other) noexcept : m_data(other.m_data) {
        other.m_data = nullptr;
    }
    ~MyString() {
        delete[] m_data;
    }
//拷贝构造 运算符版本
    MyString& operator=(const MyString& other) {
        if (this != &other) {
            char* temp = new char[strlen(other.m_data) + 1];
            strcpy(temp, other.m_data);
            delete[] m_data;
            m_data = temp;
        }
        return *this;
    }
//搬移构造 运算符版本
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] m_data;
            m_data = other.m_data;
            other.m_data = nullptr;
        }
        return *this;
    }

private:
    char* m_data;  //指针!
};

int main() {
    MyString s1 = "hello";
    MyString s2 = std::move(s1); // move constructor is called
    MyString s3 = "world";
    s1 = std::move(s3); // move assignment operator is called
    return 0;
}

技能:完美转发

前行提要

引用折叠技术:

引用折叠是 C++11 中模板和右值引用相关的一个重要特性。它描述了在模板实例化或类型别名定义过程中,不同引用类型如何合并或"折叠"成为最终的引用类型。

  1. 当两个引用类型结合时,结果是一个引用类型:  T&是函数模板 &是传参的引用符号

    • T&  & 折叠为 T&
    • T&  && 折叠为 T&
    • T&& & 折叠为 T&
    • T&& && 折叠为 T&&
  2. 当一个引用类型与一个非引用类型结合时,结果是一个引用类型:

    • T & 不会折叠
    • T && 不会折叠

总结一下:当函数需要左右值区分时 使用T&&写法,不需要左右值区分时 使用T或T& 写法。

所以我们该如何写引用折叠呢??


template <typename T>
void print(T& arg) {
    std::cout << arg << std::endl;
}


template <typename T>
void wrapper(T&& arg) {
    std::cout << arg << std::endl;

}

规范化
template <typename T>
using lref = T&;
template <typename T>
using rref = T&&;

template <typename T>
void wrapper(lref<T> arg) {
    // handle lvalue
}
template <typename T>
void wrapper(rref<T> arg) {
    // handle rvalue
}



新写法 decltype推导返回值类型
template <typename T>
auto wrapper(T&& arg) -> decltype(auto) {
    return internal_function(std::forward<T>(arg));
}

完美转发

完美转发是一种利用右值引用和模板特性实现的技术,目的是将函数参数无损地转发给内部其他函数调用。它的核心思想是:

  1. 利用模板参数推导,自动推导出最合适的引用类型
  2. 通过 std::forward 函数将参数完美地转发给内部其他函数。
示例
template <typename T>
void wrapper(T&& arg) {
    internal_function(std::forward<T>(arg));
}

template <typename T>
void internal_function(T& lvalue) {
    // handle lvalue
}

template <typename T>
void internal_function(T&& rvalue) {
    // handle rvalue
}

代码举例

#include <iostream>
#include <utility>
#include <type_traits>


//T&& 区分左右值 
template <typename T>
void print_reference_type(T&& t) {
    if (std::is_lvalue_reference_v<decltype(t)>) {
        std::cout << "t is an lvalue reference" << std::endl;
    } else {
        std::cout << "t is an rvalue reference" << std::endl;
    }
}


int main() {
    int x = 10;
    int& lref = x;
    int&& rref = 20;

    print_reference_type(10);    //r
    print_reference_type(x);     //l
    print_reference_type(lref);  //l
    print_reference_type(rref);  //l  当一个右值引用被用作左值时,它会退化为左值。
    

    //forward完美转发,模板声明符号是什么传递的左右值符号就是什么!!
    print_reference_type(std::forward<int&&>(x));    //r
    print_reference_type(std::forward<int&&>(lref)); //r
    print_reference_type(std::forward<int&&>(rref));  //r
    

    std::cout << "Type of x: " << typeid(decltype(x)).name() << std::endl;  
    std::cout << "Type of lref: " << typeid(decltype(lref)).name() << std::endl;
    std::cout << "Type of rref: " << typeid(decltype(rref)).name() << std::endl;

    return 0;
}

输出 

t is an rvalue reference
t is an lvalue reference
t is an lvalue reference
t is an lvalue reference
t is an rvalue reference
t is an rvalue reference
t is an rvalue reference
Type of x: i
Type of lref: i
Type of rref: i

总结

总的来说,C++11 及以后版本为左值和右值引入了更加细致的概念和相关特性,如左值引用、右值引用、移动语义、完美转发等。这些特性为我们提供了更加灵活和高效的编程方式,使得 C++ 在泛型编程和性能优化方面有了长足的进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值