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

1. std::as_const 概述

在 C++ 编程中,我们经常需要重载成员函数,分别提供 const 版本和非 const 版本,以便在对象是只读(const)或可变(non-const)时采取不同的行为。最经典的例子是容器类中的下标运算符 operator[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyVector 
{
public:
// 非 const 版本:用于读写
int& operator[](size_t index)
{
return data[index];
}

// const 版本:用于只读
const int& operator[](size_t index) const
{
return data[index];
}

private:
std::vector<int> data;
};

为了让代码更加简洁,开发者通常希望 非 const 版本的函数能够直接调用 const 版本的函数

在 C++17 之前,要实现这种代码复用非常麻烦,因为在一个非 const 的成员函数内部,this 指针的类型是 MyVector*非 const),如果我们直接调用 operator[],可能会导致无限递归调用非 const 版本。

解决方案:
我们需要显式地将 this 指针转换为 const 类型,即 static_cast<const MyVector&>(*this)[index]。这行代码很长且易错。std::as_const 就是为了简化这种转换而诞生的。

std::as_constC++17<utility> 头文件中引入的一个简单但强大的函数模板。它的作用只有一个:将非常量左值引用转换为常量左值引用。打一个通俗的比喻就是:它帮我们给一个变量戴上 “只读眼镜”,让我们无法意外修改它。

基本语法:

1
2
3
4
5
6
7
#include <utility>

template <class T>
constexpr add_const_t<T>& as_const(T& t) noexcept;

template <class T>
void as_const(const T&&) = delete; // 禁止对右值使用
  • 功能:接受一个引用 T&,并将其转换为 const T&
  • constexpr:函数是一个常量表达式,可以在编译期求值。
  • noexcept:函数保证不抛出异常。
  • 拒绝右值:函数特意删除了接受右值引用(T&&)的重载版本。

让我们看看 std::as_const 在标准库中是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace std 
{
// 主模板
template <class T>
constexpr add_const_t<T>& as_const(T& t) noexcept
{
return t; // 自动添加 const 限定符
}

// 删除对右值的重载
template <class T>
void as_const(const T&&) = delete;
}
  • add_const_t<T>:这是一个类型特征,给 T 添加 const 限定符
  • 返回引用:不拷贝对象,零开销
  • 禁止右值:保护临时对象不被误用

2. 应用场景

2.1 在类成员函数中复用 const 逻辑

看下面这个真实的例子。类中有一个 getData() 函数处理数据,我们希望无论对象是不是 const,都能用这个逻辑,如果是 const 返回只读,如果不是 const 返回读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <utility>
#include <vector>
#include <iostream>

class DataStore
{
public:
// 只负责获取数据的逻辑(假设逻辑很复杂,不想写两遍)
const int& getValue(size_t index) const
{
std::cout << "执行复杂的边界检查逻辑..." << std::endl;
return data.at(index);
}

// 在非 const 函数中,想复用上面的复杂逻辑,但要带回写的权限
int& getValue(size_t index)
{
return const_cast<int&>(std::as_const(*this).getValue(index));
}
private:
std::vector<int> data = {10, 20, 30};
};

int main()
{
DataStore ds;

// 调用非 const 版本
ds.getValue(1) = 99; // 修改了数据
std::cout << "修改后: " << std::as_const(ds).getValue(1) << std::endl;

return 0;
}

关于return const_cast<int&>(std::as_const(*this).getValue(index));这行代码的解释:

  1. std::as_const(*this)*this转换为 const DataStore&
  2. 调用 const 版本的 getValue(index),返回 const int&
  3. 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++ 函数重载决议中有一项基本规则:精确匹配优于需要添加限定符的匹配。

  1. const 对象可以调用 const 函数(因为 Type* 可以隐式转换为 const Type*,这意味着 可变对象可以被当做只读对象看待)。
  2. 当两个函数都存在时,非 const 对象调用非 const 函数不需要任何类型转换,是精确匹配。而调用 const 函数需要给 this 指针添加 const 限定符。

即使是非 const 对象,如果我们想强制调用 const 版本的函数(为了防止误修改,或者为了利用 const 版本的特定实现),这时需要显式地给对象加上 const 限定,这正是 std::as_const 的用武之地。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <utility>
#include <iostream>

class Test
{
public:
// 非 const 成员函数
void func()
{
std::cout << "非 const 函数被调用" << std::endl;
}

// const 成员函数
void func() const
{
std::cout << "const 函数被调用" << std::endl;
}
};

int main()
{
Test obj1; // 非 const 对象
const Test obj2; // const 对象

std::cout << "调用 obj1.func(): ";
obj1.func(); // 输出:非 const 函数被调用

std::cout << "调用 obj2.func(): ";
obj2.func(); // 输出:const 函数被调用

// 强制行为:通过 static_cast 或 std::as_const 调用 const
std::as_const(obj1).func();
// 或者
static_cast<const Test&>(obj1).func();

return 0;
}

总结一下:非 const 对象默认像找“好朋友”一样,优先找非 const 方法玩。只有在找不到非 const 方法,或者你被强制穿上了 const 外套时,它才会去找 const 方法。

2.2 防止意外修改

在使用 std::vectorstd::map 时,直接传递非引用非常危险,因为 operator[] 通常是可写的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <vector>
#include <utility>

// 这是一个配置管理器
class ConfigManager
{
public:
std::vector<int>& getSettings() { return settings_; }

private:
std::vector<int> settings_ = {100, 200, 300};
};

// 打印函数:我只想打印配置,不想改配置
void printSettings(ConfigManager& mgr)
{
// 错误做法:
// auto& s = mgr.getSettings();
// s[0] = 999; // 一旦手抖加上这行,配置就被暗中篡改了,且很难排查。

// 正确做法(防御性编程):
// 强制获取一个 const 视图
const auto& readonly_settings = std::as_const(mgr.getSettings());

std::cout << "Setting 0: " << readonly_settings[0] << std::endl;

// readonly_settings[1] = 0; // ❌ 编译报错!拦截了非法修改
}

int main()
{
ConfigManager mgr;
printSettings(mgr);
return 0;
}

2.3 配合 Range-based For 循环

配合基于范围的 For 循环也是std::as_const一个很帅的用法。当我们遍历一个容器的元素,并且明确地在循环体里对元素进行只读操作时可以用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <utility>
#include <vector>
#include <iostream>

int main()
{
std::vector<int> nums = {1, 2, 3, 4};

// 使用 std::as_const 的写法(显得比较高端,且强制只读)
for (auto&& item : std::as_const(nums))
{
std::cout << item << " ";
// item = 100; // 编译错误!item 是 const 引用
}

return 0;
}

关于这行代码 for (auto&& item : std::as_const(nums)) 是 C++ 中一种非常现代、安全且高效的遍历写法。

  1. std::as_const(nums)
    • 作用:将左值 nums 强制转换为 const std::vector<int>& 类型的右值(在表达式层面)。
    • 效果:这一步给容器戴上了一层只读面具。因为返回的是 const 引用,所以基于它产生的迭代器也是 const_iterator。这意味着遍历时,你只能读取元素,绝对无法修改 nums 中的内容。
  2. 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&

为了大家你更清楚它的定位,我们对比一下常见的写法:

  1. for (auto item : nums) ——

    • 发生拷贝。如果 vector 里存的是大对象,性能极差。
  2. for (const auto& item : nums) —— 好(常用)

    • 显式地声明了只读引用。
    • 效果和 as_const 版本几乎一样,也是零拷贝且只读。
  3. for (const auto& item : std::as_const(nums)) —— 更好(强调)

    如果不确定 nums 本身会不会在某些条件下变成非 const,使用 std::as_const 可以强制锁定来源。

  4. for (auto&& item : std::as_const(nums)) —— 最佳(现代主义)

    结合了 auto&& 的通用性和 std::as_const 的语义锁定。它告诉编译器:“给我这个容器的只读视图,并且我要用最通用的引用方式来接收元素。”

2.4 不要对右值使用

对右值(临时对象)使用std::as_const是新手最容易踩的坑。std::as_const 的参数是 T&(左值引用)。这意味着你不能直接把它作用在临时对象(右值)上。

错误代码

1
2
3
// process(std::string("hello")); 无法生成非 const 的临时对象左值绑定给 T&
// 编译错误!cannot bind non-const lvalue reference to an rvalue
process(std::as_const(std::string("hello")));

如果想把一个临时对象变 const,不能使用 std::as_const,因为临时对象本来就是纯右值,在 C++ 标准中,我们不能把右值变成左值 const 引用。解决方案是先把它存起来:

1
2
std::string temp = "hello";
process(std::as_const(temp));