1. 左值与右值

1.1 定义与分类

左值(lvalue)右值(rvalue)是C++中用于描述表达式值类别的重要概念。

  • 左值(lvalue)
    • 表示具有持久存储的对象。
    • 可以出现在赋值语句的左侧。
    • 可以被取地址(即,可以使用&运算符)。
    • 示例:变量名、引用等。
  • 右值(rvalue)
    • 表示临时对象或没有持久存储的值。
    • 通常出现在赋值语句的右侧。
    • 不能被取地址。
    • 示例:字面量、临时对象、表达式结果等。

C++11进一步细化了右值的分类:

  • 纯右值(prvalues):表示临时对象或字面量,如423.14
  • 将亡值(xvalues,expiring values):表示即将被移动的对象,如std::move的结果。

1.2 示例代码

#include <iostream>
#include <string>
#include <utility>

int main() {
    int a = 10;           // a是一个左值
    int& b = a;           // b是a的左值引用
    int&& c = 20;         // c是一个右值引用,绑定到临时右值20
    int d = a + 5;        // (a + 5)是一个纯右值

    std::string s1 = "Hello";                   // s1是一个左值
    std::string s2 = std::string("World");      // std::string("World")是一个纯右值

    std::cout << "a: " << a << ", d: " << d << std::endl;
    std::cout << "s1: " << s1 << ", s2: " << s2 << std::endl;

    // 检查类型
    std::cout << std::boolalpha;
    // 使用 decltype((a)) 获取 a 的类型,包括引用
    // `(a)` 是一个左值表达式,所以 decltype((a)) 是 `int&`
    std::cout << "a 是左值: " << std::is_lvalue_reference<decltype((a))>::value << std::endl;
    std::cout << "c 是右值引用: " << std::is_rvalue_reference<decltype(c)>::value << std::endl;

    return 0;
}

输出:

a: 10, d: 15
s1: Hello, s2: World
a 是左值: true
c 是右值引用: true

1.3 类型判断工具

C++标准库提供了几个类型特性(type traits)用于判断表达式的值类别:

  • std::is_lvalue_reference<T>:判断类型T是否为左值引用。
  • std::is_rvalue_reference<T>:判断类型T是否为右值引用。
  • std::is_lvalue<T>(C++20起):判断表达式是否为左值。
  • std::is_rvalue<T>(C++20起):判断表达式是否为右值。

示例:

#include <iostream>
#include <type_traits>

void func(int& x) {
    std::cout << "func(int&)" << std::endl;
}

void func(int&& x) {
    std::cout << "func(int&&)" << std::endl;
}

int main() {
    int a = 5;
    const int& ref = a;

    // 判断类型
    std::cout << std::boolalpha;
    // 使用 decltype((a)) 获取 a 的类型,包括引用
    // `(a)` 是一个左值表达式,所以 decltype((a)) 是 `int&`
    std::cout << "a 是左值: " << std::is_lvalue_reference<decltype((a))>::value << std::endl;
    std::cout << "ref 是左值引用: " << std::is_lvalue_reference<decltype(ref)>::value << std::endl;

    // 调用函数
    func(a);          // 调用func(int&)
    func(10);         // 调用func(int&&)

    return 0;
}

输出:

a 是左值: true
ref 是左值引用: true
func(int&)
func(int&&)

1.4 应用场景

理解左值与右值的区别是实现移动语义完美转发以及高效内存管理的基础。在编写高性能C++代码时,合理利用右值引用和移动语义可以显著提升程序的效率。


2. 模板万能引用(转发引用)

2.1 定义与特性

模板万能引用(也称为转发引用,英文为forwarding references)是C++11引入的一种引用类型,具有以下特点:

  • 表现形式为T&&,其中T是模板参数。
  • 在特定条件下,编译器会将其解析为左值引用或右值引用。
  • 能够统一处理左值和右值,适用于泛型编程中的参数传递。

重要特性

  • 当模板参数T被推导为普通类型时,T&&是右值引用。
  • 当模板参数T被推导为引用类型时,T&&会根据引用折叠规则解析为左值引用。

2.2 引用折叠规则

引用折叠是C++中的一项规则,用于处理引用类型的嵌套。主要规则如下:

  • & && &&&& & 都折叠为 &(左值引用)。
  • && && 折叠为 &&(右值引用)。

示例:

#include <type_traits>
#include <iostream>

template<typename T>
void check_referenc(T&& x){
    std::cout << std::boolalpha;
    std::cout << "T is lvalue reference: " << std::is_lvalue_reference<T>::value << std::endl;
    std::cout << "T is rvalue reference: " << std::is_rvalue_reference<T>::value << std::endl;
    std::cout << "T is rvalue : " << std::is_rvalue_reference<T&&> ::value << std::endl;
    std::cout << "x is lvalue reference: " << std::is_lvalue_reference<decltype(x)>::value << std::endl;
    std::cout << "x is rvalue reference: " << std::is_rvalue_reference<decltype(x)>::value << std::endl;
}

int main() {
    int a = 10;
    check_reference(a);          // T被推导为 int&, 因此 T&& -> int& && -> int&
    check_reference(20);         // T被推导为 int,  T&& -> int&&

    return 0;
}

输出:

T is lvalue reference: true
T is rvalue reference: false
T is rvalue : false
x is lvalue reference: true
x is rvalue reference: false
T is lvalue reference: false
T is rvalue reference: false
T is rvalue : true
x is lvalue reference: false
x is rvalue reference: true

但是要注意,如果如果我这样调用

int &&c = 100;
check_referenc(c);

输出的却是

T is lvalue reference: true
T is rvalue reference: false
T is rvalue : false
x is lvalue reference: true
x is rvalue reference: false

然而,实际中 T 被推导为 int&,这是因为 命名的右值引用变量在表达式中被视为左值

关键点:

  1. 变量的值类别(Value Category)
    • 左值(lvalue):有名称,可以出现在赋值的左侧。
    • 右值(rvalue):临时的、没有名称的值,一般不能出现在赋值的左侧。
  2. 命名的右值引用变量是左值
    • 虽然 c 的类型是 int&&,但作为一个变量名,它本身是一个左值。
    • 这意味着在表达式中使用 c 时,c 被视为左值,而不是右值。
  3. 类型推导规则

    • 当模板参数使用T&&(被称为转发引用或万能引用)时,类型推导遵循特定规则:
      • 如果传入的是左值T 被推导为 T&
      • 如果传入的是右值T 被推导为 T(非引用类型)。
  4. 传入左值(c

    • c 是一个命名的右值引用变量,但作为表达式它是左值。
    • 因此,T 被推导为 int&
    • T&& 则根据引用折叠规则(int& && => int&)。

为了让 T 被推导为右值引用,我们需要传递一个 真正的右值。这可以通过使用 std::move 来实现:

// std::move(c) 是右值,T 被推导为 int
check_reference(std::move(c));

输出

T is lvalue reference: false
T is rvalue reference: false
T is rvalue : true
x is lvalue reference: false
x is rvalue reference: true
  1. 传递右值 std::move(c)
    • std::move(c)c 转换为 int&&(右值)。
    • T 被推导为 int(非引用类型)。
    • std::is_lvalue_reference<T>::valuefalse
    • std::is_rvalue_reference<T>::valuefalse
    • std::is_rvalue_reference<T&&>::value 相当于 std::is_rvalue_reference<int&&>::value,为 true
    • decltype(x) 相当于 int&&,所以 x 被识别为右值引用。
  2. 为什么T被实例化为int而不是int&&

在模板参数中,当一个类型参数 TT&& 的形式出现,并且 T 是模板参数时,T&& 被称为 转发引用(有时称为万能引用)。转发引用具有特殊的类型推导规则:

  • 如果传递的是左值T 被推导为 T&
  • 如果传递的是右值T 被推导为 T不带引用)。

模板类型推导的具体规则

根据 C++ 标准(C++17 标准草案):

如果 P 是 T&&,并且 A(实参类型)是一个非引用类型的表达式,则 T 被推导为 A 的类型。

如果 P 是 T&&,并且 A 是一个引用类型的表达式,则去除引用后的类型用于推导。

具体到我们的例子:

  • 传递 c(一个左值,类型 int&&):
    • 表达式 c 是一个 左值
    • 当传递左值给 T&& 时,T 被推导为 int&(即 int 加上引用)。
    • 因此,参数类型 T&& 实际上是 int& &&,根据引用折叠规则,int& && 合并为 int&
  • 传递 std::move(c)(一个右值,类型 int&&):
    • 表达式 std::move(c) 是一个 右值
    • 当传递右值给 T&& 时,T 被推导为 int(不带引用)。
    • 因此,参数类型 T&&int&&

也可以采用原样转发,达到和move相同的效果

check_referenc(std::forward<int&&>(c));

输出

T is lvalue reference: false
T is rvalue reference: false
T is rvalue : true
x is lvalue reference: false
x is rvalue reference: true

2.3 示例代码

以下示例展示了如何使用模板万能引用编写能够接受任意类型参数的函数,并通过转发保持参数的原有值类别。

#include <iostream>
#include <string>
#include <utility>

// 目标函数,分别有左值和右值的重载
void process(int&x){
    std::cout << "process(int&)" << std::endl;
}

void process(int&& x){
    std::cout << "process(int&&)" << std::endl;
}

// 通用的包装函数,使用转发引用
template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg)); // 原样转发
}

int main() {
    int a = 10;
    wrapper(a);          // 传递左值,调用process(int&)
    wrapper(20);         // 传递右值,调用process(int&&)
    return 0;
}

输出:

process(int&)
process(int&&)

在上述代码中,wrapper函数使用模板万能引用T&&接收参数,通过std::forward<T>(arg)实现原样转发,确保传递给process函数的参数保持其原有的值类别。

如果我们把wrapper改为

// 通用的包装函数,使用转发引用
template<typename T>
void wrapper(T&& arg) {
    process(arg); // 原样转发
}

调用

int main() {
    int a = 10;
    wrapper(a);          // 传递左值,调用process(int&)
    wrapper(20);         // 传递右值,调用process(int&&)
    return 0;
}

则输出

process(int&)
process(int&)

因为虽然wrapper(20)调用时会将T实例化为int类型, arg变为int && 类型,但是arg本身是一个左值, 所以调用process会选择左值引用版本

// 目标函数
void process(int&x){
    std::cout << "process(int&)" << std::endl;
}

3. 类型推导(Type Deduction)

3.1 类型推导规则

在C++模板编程中,类型推导是指编译器根据传入的实参自动推导模板参数的类型。类型推导遵循以下基本规则:

  1. 模板参数的推导
    • 当模板参数与函数参数进行匹配时,编译器会根据实参类型推导出模板参数类型T
  2. 引用的处理
    • 如果函数参数采用右值引用(如T&&),且传递的是左值,则T会被推导为左值引用类型。
    • 如果传递的是右值,则T会被推导为非引用类型。
  3. cv限定符的去除
    • 类型推导会去除顶层的constvolatile修饰。

3.2 与引用的关系

在模板参数中使用引用类型时,类型推导会涉及引用折叠规则。例如,当一个模板函数参数为T&T&&时,传入参数的值类别会影响T的推导结果。

示例:

#include <iostream>
#include <type_traits>

template<typename T>
void deduce_type(T&&) {
    std::cout << std::boolalpha;
    std::cout << "Is T an lvalue reference? " << std::is_lvalue_reference<T>::value << std::endl;
    std::cout << "Is T an rvalue reference? " << std::is_rvalue_reference<T>::value << std::endl;
}

int main() {
    int a = 5;
    const int& ref = a;

    deduce_type(a);            // T被推导为 int&
    deduce_type(10);           // T被推导为 int
    deduce_type(ref);          // T被推导为 const int&
    deduce_type(std::move(a)); // T被推导为 int

    return 0;
}

输出:

Is T an lvalue reference? true
Is T an rvalue reference? false
Is T an lvalue reference? false
Is T an rvalue reference? true
Is T an lvalue reference? true
Is T an rvalue reference? false
Is T an lvalue reference? false
Is T an rvalue reference? true

3.3 示例代码

以下示例展示了不同情况下模板参数T的推导结果,以及如何利用类型推导编写通用函数。

#include <iostream>
#include <type_traits>

template<typename T>
void display_type(T&& param) {
    std::cout << "Type of T: " 
              << (std::is_lvalue_reference<T>::value ? "lvalue reference" : "rvalue reference") 
              << ", "
              << (std::is_integral<typename std::remove_reference<T>::type>::value ? "Integral" : "Non-Integral")
              << std::endl;
}

int main() {
    int x = 10;
    const int& y = x;
    display_type(x);            // T被推导为 int&
    display_type(20);           // T被推导为 int&&
    display_type(y);            // T被推导为 const int&
    display_type(std::move(x)); // T被推导为 int&&

    return 0;
}

输出:

Type of T: lvalue reference, Integral
Type of T: rvalue reference, Integral
Type of T: lvalue reference, Integral
Type of T: rvalue reference, Integral

在此示例中,通过模板函数display_type,我们可以观察到不同参数传递方式下,模板参数T的推导结果。


4. 原样转发(Perfect Forwarding)

4.1 定义与作用

原样转发(Perfect Forwarding)是指在模板函数中,将接收到的参数以其原有的值类别(左值或右值)传递给另一个函数。这项技术确保了泛型代码能够像手写特定代码那样高效和正确地处理参数。

作用

  • 保持参数的值类别,确保正确调用函数重载。
  • 利用移动语义,避免不必要的拷贝,提升性能。
  • 编写通用、复用性强的代码。

4.2 实现原理

原样转发通常结合模板万能引用(转发引用)和std::forward来实现。具体流程如下:

  1. 使用模板万能引用(T&&)接收参数。

  2. 使用

    std::forward<T>(arg)
    

    将参数转发给目标函数。

    • std::forward根据T的类型,将参数转换为左值或右值。
    • 如果参数原本是左值,std::forward返回左值引用。
    • 如果参数原本是右值,std::forward返回右值引用。

4.3 为什么需要原样转发

在泛型编程中,函数模板可能需要将接收到的参数传递给其他函数。若不使用原样转发,参数会失去原有的值类别信息,可能导致以下问题:

  • 右值参数被当作左值处理,无法利用移动语义,导致性能下降。
  • 无法正确调用目标函数的重载版本。
  • 增加不必要的拷贝开销,影响程序性能。

通过原样转发,可以确保参数在传递过程中保持其原有的左值或右值特性,提升代码的效率和灵活性。

4.4 示例代码

以下示例展示了如何实现一个通用的wrapper函数,通过原样转发将参数传递给目标函数,同时保留参数的值类别。

#include <iostream>
#include <string>
#include <utility>

// 目标函数,分别有左值和右值的重载
void process(const std::string& s) {
    std::cout << "Processing lvalue: " << s << std::endl;
}

void process(std::string&& s) {
    std::cout << "Processing rvalue: " << s << std::endl;
}

// 通用的包装函数,实现原样转发
template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));
}

int main() {
    std::string name = "Alice";
    wrapper(name);                     // 传递左值,调用process(const std::string&)
    wrapper("Bob");                    // 传递右值,调用process(std::string&&)
    wrapper(std::move(name));          // 传递右值,调用process(std::string&&)

    return 0;
}

输出:

Processing lvalue: Alice
Processing rvalue: Bob
Processing rvalue: Alice

在上述代码中:

  • wrapper(name)传递的是左值,T被推导为std::string&std::forward保持其为左值引用,调用process(const std::string&)
  • wrapper("Bob")传递的是右值,T被推导为const char*,在process函数重载的选择上,这里简单化处理为调用接收右值的版本(视具体实现而定)。
  • wrapper(std::move(name))传递的是右值,T被推导为std::stringstd::forward将其转换为右值引用,调用process(std::string&&)

4.5 泛型工厂函数示例

以下示例展示了如何使用原样转发实现一个泛型工厂函数,完美转发构造函数的参数,以高效创建对象。

#include <iostream>
#include <string>
#include <utility>

// 类的定义
class Person {
public:
    std::string name;
    int age;

    // 左值引用构造函数
    Person(const std::string& n, int a) : name(n), age(a) {
        std::cout << "Constructed Person(const std::string&, int)" << std::endl;
    }

    // 右值引用构造函数
    Person(std::string&& n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed Person(std::string&&, int)" << std::endl;
    }
};

// 工厂函数,使用原样转发构造函数参数
template<typename T, typename... Args>
T create(Args&&... args) {
    return T(std::forward<Args>(args)...);
}

int main() {
    std::string name = "Alice";

    // 传递左值
    Person p1 = create<Person>(name, 30); 
    // 传递右值
    Person p2 = create<Person>(std::string("Bob"), 25); 

    return 0;
}

输出:

Constructed Person(const std::string&, int)
Constructed Person(std::string&&, int)

在此示例中:

  • create<Person>(name, 30)传递的是左值,调用Person(const std::string&, int)构造函数。
  • create<Person>(std::string("Bob"), 25)传递的是右值,调用Person(std::string&&, int)构造函数。

通过原样转发,create函数能够根据传入参数的值类别,调用相应的构造函数,实现高效的对象创建。

4.6 避免不必要的拷贝

以下示例展示了如果不使用原样转发,可能导致的多次拷贝问题。

#include <iostream>
#include <string>

// 类的定义
class BigObject {
public:
    std::string data;

    BigObject(const std::string& d) : data(d) {
        std::cout << "BigObject constructed with lvalue" << std::endl;
    }

    BigObject(std::string&& d) : data(std::move(d)) {
        std::cout << "BigObject constructed with rvalue" << std::endl;
    }
};

// 处理函数,左值和右值的重载
void process(const BigObject& obj) {
    std::cout << "Processing lvalue BigObject" << std::endl;
}

void process(BigObject&& obj) {
    std::cout << "Processing rvalue BigObject" << std::endl;
}

// 包装函数,不使用原样转发
template<typename T>
void bad_wrapper(T&& obj) {
    process(obj); // 始终以左值形式传递
}

// 包装函数,使用原样转发
template<typename T>
void good_wrapper(T&& obj) {
    process(std::forward<T>(obj)); // 原样转发
}

int main() {
    std::string s = "Sample data";

    // 使用bad_wrapper
    BigObject bo1 = BigObject(s);              // 使用lvalue构造
    BigObject bo2 = BigObject(std::move(s));   // 使用rvalue构造

    bad_wrapper(bo1); // 始终作为左值处理
    bad_wrapper(BigObject("Temp")); // 作为左值处理,浪费移动语义

    // 使用good_wrapper
    good_wrapper(bo1); // 作为左值处理
    good_wrapper(BigObject("Temp")); // 作为右值处理,利用移动语义

    return 0;
}

输出:

BigObject constructed with lvalue
BigObject constructed with rvalue
Processing lvalue BigObject
Processing lvalue BigObject
BigObject constructed with rvalue
Processing rvalue BigObject

在此示例中:

  • bad_wrapper函数不使用std::forward,导致即使传递的是右值,process函数也以左值形式接收,无法利用移动语义。
  • good_wrapper函数使用std::forward,正确保持参数的值类别,允许process函数调用右值重载,从而利用移动语义,提高性能。

5. 总结

  • 左值与右值:理解值类别是掌握C++移动语义、资源管理和高效编程的基础。左值代表具有持久存储的对象,而右值通常是临时对象。
  • 模板万能引用(转发引用):通过T&&形式的模板参数,可以统一处理左值和右值,适用于泛型编程中的参数传递。引用折叠规则决定了T&&在不同上下文中的解析方式。
  • 类型推导:编译器根据实参自动推导模板参数类型,涉及引用类型时需要理解引用折叠规则和类型推导的细节。
  • 原样转发(Perfect Forwarding):结合模板万能引用和std::forward,确保在泛型函数中将参数以其原有值类别传递给目标函数,避免不必要的拷贝,提升代码效率和灵活性。

results matching ""

    No results matching ""