C++11 类型分类

C++11 以后,进一步对类型进行了划分,右值被细分为 纯右值 (prvalue)将亡值 (xvalue)

1. 纯右值 (pure value, 简称 prvalue)
  • 定义:指那些字面值常量,或求值结果相当于字面值,或是一个不具名的临时对象。
  • 等价关系:C++11 中的纯右值概念划分等价于 C++98 中的右值。
  • 常见示例
    • 字面值:42truenullptr
    • 不具名的临时对象或运算结果:str.substr(1, 2)str1 + str2
    • 传值返回的函数调用
    • 内置类型的运算:如整型 aba++a + b
2. 将亡值 (expiring value, 简称 xvalue)
  • 定义:指返回右值引用的函数的调用表达式,以及转换为右值引用的转换函数的调用表达式。
  • 常见示例
    • std::move(x)
    • static_cast<X&&>(x)
3. 泛左值 (generalized lvalue, 简称 glvalue)
  • 定义:泛左值是一个更宽泛的概念,它包含了 将亡值 (xvalue) 和传统的 左值 (lvalue)
    在这里插入图片描述

C++ 引用折叠 (Reference Collapsing)

1. 基本概念与规则
  • 背景:C++ 中不能直接定义“引用的引用”,例如 int& && r = i; 会直接报错。但通过 模板typedef 中的类型操作可以间接构成引用的引用。
  • 折叠规则:当通过模板或 typedef 构成引用的引用时,C++11 给出了明确的折叠规则:
    • 右值引用的右值引用 折叠成 右值引用 (&& && -> &&)。
    • 所有其他组合 均折叠成 左值引用 (& &, & &&, && & -> &)。

2. typedef 与函数模板中的折叠演示

以下程序展示了在 typedef 和函数模板中,引用折叠的具体表现:

// 由于引用折叠限定,f1实例化以后总是一个左值引用 
template<class T>
void f1(T& x) {}

// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用 
template<class T>
void f2(T&& x) {}

int main()
{
    typedef int& lref;
    typedef int&& rref;
    int n = 0;

    // --- typedef 中的折叠规则 ---
    lref& r1 = n;  // r1 的类型是 int& (左值引用的左值引用 -> 左值引用)
    lref&& r2 = n; // r2 的类型是 int& (左值引用的右值引用 -> 左值引用)
    rref& r3 = n;  // r3 的类型是 int& (右值引用的左值引用 -> 左值引用)
    rref&& r4 = 1; // r4 的类型是 int&& (右值引用的右值引用 -> 右值引用)

    // --- 模板 f1 的折叠演示 ---
    // 没有折叠 -> 实例化为 void f1(int& x) 
    f1<int>(n);
    // f1<int>(0); // 报错,0为右值,无法传参

    // 折叠 -> 实例化为 void f1(int& x) 
    f1<int&>(n);
    // f1<int&>(0); // 报错

    // 折叠 -> 实例化为 void f1(int& x) 
    f1<int&&>(n);
    // f1<int&&>(0); // 报错

    // 折叠 -> 实例化为 void f1(const int& x) 
    f1<const int&>(n);
    f1<const int&>(0); // const左值引用可以正常引用右值

    // 折叠 -> 实例化为 void f1(const int& x) 
    f1<const int&&>(n);
    f1<const int&&>(0);

    // --- 模板 f2 的折叠演示 ---
    // 没有折叠 -> 实例化为 void f2(int&& x) 
    // f2<int>(n); // 报错,右值引用不可以引用左值
    f2<int>(0);

    // 折叠 -> 实例化为 void f2(int& x) 
    f2<int&>(n);
    // f2<int&>(0); // 报错,0为右值,无法传参

    // 折叠 -> 实例化为 void f2(int&& x) 
    // f2<int&&>(n); // 报错
    f2<int&&>(0);

    return 0;
}

万能引用 (Universal Reference)

在像 f2 这样的函数模板中,T&& x 参数看起来像是一个普通的右值引用参数,但由于引用折叠规则的存在,它的实际行为非常特殊:

  • 当传递 左值 时,它就是 左值引用
  • 当传递 右值 时,它就是 右值引用

因此,有些地方也把这种函数模板的参数叫做 万能引用

推导原理

Function(T&& t) 函数模板程序中,其背后的推导逻辑如下:

  1. 实参为右值时
    假设实参是 int 右值,模板参数 T 会被推导为 int。结合引用折叠规则,最终实例化出 右值引用 版本的函数。
  2. 实参为左值时
    假设实参是 int 左值,模板参数 T 会被推导为 int&。结合引用折叠规则(int& && -> int&),最终实例化出 左值引用 版本的函数。

完美转发 (Perfect Forwarding)

1. 为什么要使用完美转发?

Function(T&& t) 函数模板程序中:

  • 传左值实例化以后是左值引用的 Function 函数。
  • 传右值实例化以后是右值引用的 Function 函数。

存在的问题
变量表达式都是左值属性。这意味着一个右值被右值引用绑定后,右值引用变量(即函数形参 t)本身的属性是左值。如果我们直接把 t 传递给下一层函数 Fun,那么匹配的永远都是左值引用版本的 Fun 函数。

为了在转发过程中保持 t 对象原始的左右值属性,我们需要使用完美转发。

2. std::forward 的实现原理

std::forward 本质是一个函数模板,主要通过引用折叠的方式实现。其核心源码简化如下:

template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept 
{ 
    // 将参数无条件强转为 _Ty&&,利用引用折叠规则还原原始类型
    return static_cast<_Ty&&>(_Arg);
}

C++11 可变参数模板总结

1. 基本语法及原理

C++11 支持可变参数模板,即支持可变数量参数的函数模板和类模板。可变数目的参数被称为参数包,主要分为两种:

  • 模板参数包:表示零或多个模板参数。
  • 函数参数包:表示零或多个函数参数。
2. 常见语法格式

使用省略号 ... 来指出一个模板参数或函数参数表示一个包。

// 基础语法
template <class ...Args> void Func(Args... args) {}

// 左值引用形式
template <class ...Args> void Func(Args&... args) {}

// 右值引用形式(完美转发常用)
template <class ...Args> void Func(Args&&... args) {}

获取参数个数

可以使用 sizeof… 运算符来计算参数包中参数的个数。

template <class ...Args>
void Print(Args&&... args)
{
    cout << sizeof...(args) << endl;
}

int main()
{
    double x = 2.2;
    Print();                     // 包里有0个参数 
    Print(1);                    // 包里有1个参数 
    Print(1, string("xxxxx"));   // 包里有2个参数 
    Print(1.1, string("xxxxx"), x); // 包里有3个参数 
    return 0;
}

C++11 参数包的递归展开

在 C++11 中,参数包是不能直接用 for 循环遍历的(因为参数包的解析是编译时的行为,而 for 循环是运行时的逻辑)。因此,我们通常使用递归的方式,像“剥洋葱”一样把参数包里的元素一个个取出来。

实现递归展开通常需要三个部分:

  1. 外层接收函数:接收完整的参数包。
  2. 递归展开函数:每次提取参数包的第一个元素,剩下的继续递归。
  3. 终止函数:当参数包为空时,结束递归。
示例:打印参数包内容
#include <iostream>
#include <string>
using namespace std;

// 1. 递归的终止条件(当参数包被剥完,变为空时,匹配这个普通函数)
void ShowList() {
    cout << endl; // 打印完所有参数后换行
}

// 2. 递归展开的主模板
// 每次取出一个参数 x,剩下的参数包是 args
template <class T, class ...Args>
void ShowList(T x, Args... args) {
    cout << x << " ";       // 处理当前拿到的第一个参数
    ShowList(args...);      // 触发包扩展:将剩下的参数包继续递归传递
}

// 3. 外层接收函数(可选,方便统一调用)
template <class ...Args>
void Print(Args... args) {
    ShowList(args...);      // 触发第一次包扩展
}

int main() {
    Print();                            // 输出空行
    Print(1);                           // 输出:1 
    Print(1, string("hello"));          // 输出:1 hello 
    Print(1, string("hello"), 3.14);    // 输出:1 hello 3.14 
    return 0;
}

C++11 emplace 系列接口总结

1. 接口声明
  • template <class... Args> void emplace_back(Args&&... args);
  • template <class... Args> iterator emplace(const_iterator position, Args&&... args);
2. 核心优势
  • 原地构造:直接在容器内部内存中构造对象,避免了先构造临时对象再进行拷贝/移动的开销。
  • 完美转发:传递参数包时必须使用 std::forward<Args>(args)...,防止右值引用在编译展开时退化为左值。
3. 模拟实现核心

在自定义容器的节点构造中,直接接收并完美转发参数包给数据类型 T 的构造函数:

// 节点的可变参数模板构造
template <class... Args>
ListNode(Args&&... args)
    : _data(std::forward<Args>(args)...) // 核心:完美转发参数包
{}

// emplace_back 接口实现
template <class... Args>
void emplace_back(Args&&... args)
{
    insert(end(), std::forward<Args>(args)...);
}

传左值/右值对象:emplace_back(obj) 等同于 push_back(obj),走拷贝/移动构造。
传构造参数包:emplace_back(args…) 是 push_back 做不到的,它能直接在容器内存中构造对象,效率最高。

更多推荐