左值与右值

首先需要了解这两个概念:
==左值是指可以出现在赋值操作符左边的对象==。简而言之,左值表示一个对象的持久位置,即内存中的某个位置。==左值有明确的生命周期,并且可以通过它来获取对象的引用。==

  • 可以取地址:左值可以取地址(& 操作符)。
  • 持久性:左值在程序的某个范围内存在,并且可以在多个地方使用。
    int a = 10;
    int &b = a;  // 'a' 是左值,'b' 是 'a' 的引用
    b = 20;  // 可以将 20 赋值给 'a'
    右值是指临时对象或值,通常出现在赋值操作符的右边。==右值代表了一个临时的、不持久的对象,它没有明确的持久内存地址,因此不能取地址==。右值的生命周期非常短,通常在表达式结束时即被销毁。
  • 无法取地址:右值不能使用 & 获取地址。
  • 临时性:右值通常是函数的返回值、临时变量、常量等。
    int a = 10;
    a = 20;  // '20' 是右值

    左值引用与右值引用

  1. 左值引用(Lvalue Reference):通常是指向对象的引用,表示一个已经存在的对象。
    语法为 T&,例如:int& x = a;,这里 x 是对 a 的左值引用。
  2. 右值引用(Rvalue Reference):通过 && 语法表示的引用,==它允许“绑定”到右值上,也就是临时对象。右值引用支持移动语义,这意味着可以将资源从一个对象转移到另一个对象,而不是进行昂贵的拷贝。==
    语法:T&&,例如:int&& x = 10;,这里 x 是对右值 10 的右值引用。

    完美转发

    完美转发是一个技术,通过它我们可以==将函数参数完美地传递给另一个函数,保留参数的左值/右值性质==。换句话说,完美转发确保我们不会错误地修改参数的值类别(左值还是右值),并且不会丢失性能。
    典型场景
    完美转发通常出现在模板函数中,尤其是在工厂函数、构造函数、或者处理某些资源的情况下。当你在一个函数中收到参数并希望将该参数转发给另一个函数时,如果使用普通的引用传递,可能会丢失右值的信息,导致不必要的拷贝。所以可以使用 std::forward右值引用 来实现完美转发。示例如下:
    #include <iostream>
    #include <utility>  // std::forward
    
    // 一个简单的打印函数,接受左值引用和右值引用
    void print(int& x) {
        std::cout << "Lvalue: " << x << std::endl;
    }
    
    void print(int&& x) {
        std::cout << "Rvalue: " << x << std::endl;
    }
    
    // 完美转发函数
    template <typename T>
    void forward_to_print(T&& arg) {
        print(std::forward<T>(arg));  // 使用 std::forward 完美转发
    }
    
    int main() {
        int a = 10;
        forward_to_print(a);  // Lvalue 被转发
        forward_to_print(20); // Rvalue 被转发
    }
    
  • std::forward<T>(arg):根据 T 的类型,决定是否转发为左值引用或右值引用。
  • 模板推导:在 forward_to_print 中,T&& arg 会根据传递的参数类型推导为 int&int&&
  • 如果传递的是左值,T 会被推导为 int&std::forward<T>(arg) 会转发为 int&,即左值引用。
  • 如果传递的是右值,T 会被推导为 intstd::forward<T>(arg) 会转发为 int&&,即右值引用。
    为什么需要完美转发?
    完美转发在以下情况下非常有用:
  1. 避免不必要的拷贝
    • 如果你不使用完美转发,而是直接传递参数,那么函数参数可能会被不必要地拷贝。特别是在处理资源密集型对象时(如大型容器或自定义类),拷贝可能会引发性能问题。
  2. 保留参数的右值/左值性质
    • 通过完美转发,可以确保在转发参数时不会丢失原始的左值/右值性质。这样,右值可以被“移动”,左值可以被正确引用,避免不必要的资源拷贝。
  3. 提高代码的通用性
    • 完美转发能够让你的代码更加通用,可以处理多种类型的输入参数,无论是左值还是右值。
      工厂函数:当你创建一个对象并返回时,可以使用完美转发来传递参数,以便构造该对象。
      示例:
      template <typename T, typename... Args>
      std::unique_ptr<T> make_unique(Args&&... args) {
          return std::make_unique<T>(std::forward<Args>(args)...);
      }

      LLVM 场景中的应用

      IR 构造器(IRBuilder)的指令创建
      LLVM 中的 IRBuilder 类负责生成各种中间表示(IR)指令。为了支持不同类型的参数以及避免拷贝,IRBuilder 内部常使用模板函数和完美转发。例如,在创建一个算术运算指令时,IRBuilder 可能定义类似下面的模板函数:
      template<typename... Args>
      Instruction *IRBuilder::CreateAdd(Args &&...args) {
          // 通过完美转发,将参数传递给指令构造函数
          return new AddInst(std::forward<Args>(args)...);
      }
  • 当用户调用 CreateAdd 并传入参数时,这些参数可能是左值也可能是右值。使用 std::forward 可以确保参数以正确的方式转发给 AddInst 的构造函数。
  • 如果参数是临时对象(右值),那么转发时会保留右值的性质,从而使得内部的构造函数可以进行移动操作,避免不必要的拷贝开销。
  • 这种写法使得 IRBuilder 能够灵活支持多种参数类型,同时保证代码性能和资源管理的高效性。
    容器类中的 emplace 功能
    LLVM 的 ADT 库中,比如 SmallVector 或其他容器,也大量使用了类似于 emplace_back 的接口。该接口允许直接在容器内构造对象,而不是先构造后拷贝或移动。示例如下:
    template<typename T, typename... Args>
    void SmallVector<T>::emplace_back(Args &&...args) {
        // 在容器的内存空间上直接构造对象,避免额外的拷贝
        new (data() + size()) T(std::forward<Args>(args)...);
        ++size_;
    }
  • emplace_back 接受任意参数,并将它们完美转发给类型 T 的构造函数。
  • 这样做的好处是:当传入的是临时对象时,可以直接移动构造;当传入的是左值时,则通过拷贝构造。
  • 完美转发在这里保证了对象创建时能最大程度地利用传入参数的特性,从而提升性能和减少内存开销。