将左值引用转换为常量引用 - std::as_const

将左值引用转换为常量引用 - std::as_const
苏丙榅1. std::as_const 概述
在 C++ 编程中,我们经常需要重载成员函数,分别提供 const 版本和非 const 版本,以便在对象是只读(const)或可变(non-const)时采取不同的行为。最经典的例子是容器类中的下标运算符 operator[]:
1 | class MyVector |
为了让代码更加简洁,开发者通常希望 非 const 版本的函数能够直接调用 const 版本的函数。
在 C++17 之前,要实现这种代码复用非常麻烦,因为在一个非 const 的成员函数内部,this 指针的类型是 MyVector*(非 const),如果我们直接调用 operator[],可能会导致无限递归调用非 const 版本。
解决方案:
我们需要显式地将 this 指针转换为 const 类型,即 static_cast<const MyVector&>(*this)[index]。这行代码很长且易错。std::as_const 就是为了简化这种转换而诞生的。
std::as_const 是 C++17 在 <utility> 头文件中引入的一个简单但强大的函数模板。它的作用只有一个:将非常量左值引用转换为常量左值引用。打一个通俗的比喻就是:它帮我们给一个变量戴上 “只读眼镜”,让我们无法意外修改它。
基本语法:
1 |
|
- 功能:接受一个引用
T&,并将其转换为const T&。 constexpr:函数是一个常量表达式,可以在编译期求值。noexcept:函数保证不抛出异常。- 拒绝右值:函数特意删除了接受右值引用(
T&&)的重载版本。
让我们看看 std::as_const 在标准库中是如何实现的:
1 | namespace std |
add_const_t<T>:这是一个类型特征,给T添加const限定符- 返回引用:不拷贝对象,零开销
- 禁止右值:保护临时对象不被误用
2. 应用场景
2.1 在类成员函数中复用 const 逻辑
看下面这个真实的例子。类中有一个 getData() 函数处理数据,我们希望无论对象是不是 const,都能用这个逻辑,如果是 const 返回只读,如果不是 const 返回读写。
1 |
|
关于return const_cast<int&>(std::as_const(*this).getValue(index));这行代码的解释:
std::as_const(*this)将*this转换为const DataStore&- 调用
const版本的getValue(index),返回const int& const_cast去掉const属性,返回int&
这里仍然需要 const_cast,是因为我们需要把返回值的只读属性去掉,这是安全的,因为原始对象本身就不是 const 的。
如果没有 std::as_const,我们必须写成:const_cast<int&>(((const DataStore&)(*this)).getValue(index)),这简直是噩梦。
2.2 非 const 对象调用 const 函数
在 C++ 中对象类型(const或非const)和可调用的方法(const或非const)存在如下关系:
| 对象类型 | 可以调用 const 方法? |
可以调用非 const 方法? |
|---|---|---|
| const 对象 | ✅ 是 | ❌ 否 (编译报错) |
| 非 const 对象 | ✅ 是 | ✅ 是 |
C++ 函数重载决议中有一项基本规则:精确匹配优于需要添加限定符的匹配。
- 非
const对象可以调用const函数(因为Type*可以隐式转换为const Type*,这意味着 可变对象可以被当做只读对象看待)。 - 当两个函数都存在时,非
const对象调用非const函数不需要任何类型转换,是精确匹配。而调用const函数需要给this指针添加const限定符。
即使是非 const 对象,如果我们想强制调用 const 版本的函数(为了防止误修改,或者为了利用 const 版本的特定实现),这时需要显式地给对象加上 const 限定,这正是 std::as_const 的用武之地。
1 |
|
总结一下:非 const 对象默认像找“好朋友”一样,优先找非 const 方法玩。只有在找不到非 const 方法,或者你被强制穿上了 const 外套时,它才会去找 const 方法。
2.2 防止意外修改
在使用 std::vector 或 std::map 时,直接传递非引用非常危险,因为 operator[] 通常是可写的。
1 |
|
2.3 配合 Range-based For 循环
配合基于范围的 For 循环也是std::as_const一个很帅的用法。当我们遍历一个容器的元素,并且明确地在循环体里对元素进行只读操作时可以用它。
1 |
|
关于这行代码 for (auto&& item : std::as_const(nums)) 是 C++ 中一种非常现代、安全且高效的遍历写法。
std::as_const(nums)- 作用:将左值
nums强制转换为const std::vector<int>&类型的右值(在表达式层面)。 - 效果:这一步给容器戴上了一层只读面具。因为返回的是
const引用,所以基于它产生的迭代器也是const_iterator。这意味着遍历时,你只能读取元素,绝对无法修改nums中的内容。
- 作用:将左值
auto&& item- 这是什么?这被称为转发引用或万能引用。
- 为什么这么写?
auto&&会根据初始化它的表达式的值类别(左值/右值)和const属性自动推导:- 如果右边是
T&,item就变成T&。 - 如果右边是
const T&,item就变成const T&。 - 如果右边是
T&&,item就变成T&&。
- 如果右边是
在本例中std::as_const(nums)返回的是const容器,解引用出来的元素是const int&,所以 auto&& 会自动将其推导为 const int&。
为了大家你更清楚它的定位,我们对比一下常见的写法:
for (auto item : nums)—— 差- 发生拷贝。如果
vector里存的是大对象,性能极差。
- 发生拷贝。如果
for (const auto& item : nums)—— 好(常用)- 显式地声明了只读引用。
- 效果和
as_const版本几乎一样,也是零拷贝且只读。
for (const auto& item : std::as_const(nums))—— 更好(强调)如果不确定
nums本身会不会在某些条件下变成非const,使用std::as_const可以强制锁定来源。for (auto&& item : std::as_const(nums))—— 最佳(现代主义)结合了
auto&&的通用性和std::as_const的语义锁定。它告诉编译器:“给我这个容器的只读视图,并且我要用最通用的引用方式来接收元素。”
2.4 不要对右值使用
对右值(临时对象)使用std::as_const是新手最容易踩的坑。std::as_const 的参数是 T&(左值引用)。这意味着你不能直接把它作用在临时对象(右值)上。
错误代码:
1 | // process(std::string("hello")); 无法生成非 const 的临时对象左值绑定给 T& |
如果想把一个临时对象变 const,不能使用 std::as_const,因为临时对象本来就是纯右值,在 C++ 标准中,我们不能把右值变成左值 const 引用。解决方案是先把它存起来:
1 | std::string temp = "hello"; |


















