重载下标运算符

在操作数组时,我们通常使用下标运算符([])来索引数组中的特定元素:

myArray[0] = 7; // 将 7 存入数组的第一个元素

然而,考虑以下 IntList 类,其成员变量为数组:

class IntList
{
private:
    int m_list[10]{};
};

int main()
{
    IntList list{};
    // 如何访问 m_list 中的元素?
    return 0;
}

由于成员变量 m_list 为私有,我们无法通过变量 list 直接访问它,因此无法直接对 m_list 数组进行读取或赋值。那么,如何向列表中存取元素?

未使用运算符重载时的典型做法

通常做法是编写访问函数:

class IntList
{
private:
    int m_list[10]{};

public:
    void setItem(int index, int value) { m_list[index] = value; }
    int getItem(int index) const { return m_list[index]; }
};

虽然可行,但并不友好。例如:

int main()
{
    IntList list{};
    list.setItem(2, 3);

    return 0;
}

我们究竟是将元素 2 赋值为 3,还是将元素 3 赋值为 2?在未查看 setItem() 定义的情况下,难以判断。

也可以返回整个数组再使用 operator[] 访问:

class IntList
{
private:
    int m_list[10]{};

public:
    int* getList() { return m_list; }
};

但语法怪异:

int main()
{
    IntList list{};
    list.getList()[2] = 3;

    return 0;
}

重载 operator[]

更佳方案是重载下标运算符([]),以允许直接访问 m_list 元素。下标运算符必须重载为成员函数。重载函数始终接受一个参数:用户置于方括号内的下标。对于 IntList,该参数应为整型索引,函数返回对应整型值。

#include <iostream>

class IntList
{
private:
    int m_list[10]{};

public:
    int& operator[] (int index)
    {
        return m_list[index];
    }
};

/*
// 亦可在类外实现
int& IntList::operator[] (int index)
{
    return m_list[index];
}
*/

int main()
{
    IntList list{};
    list[2] = 3;          // 设置值
    std::cout << list[2] << '\n'; // 获取值

    return 0;
}

此后,对类对象使用下标运算符时,编译器返回 m_list 对应元素,实现直接读写。

语法直观:表达式 list[2] 中,编译器先检查是否存在重载 operator[],若存在,则将 2 作为参数传入。

注意:虽可为函数参数提供默认值,但语法上不允许 operator[] 后无下标,因此无意义。

为何 operator[] 返回引用

考察 list[2] = 3 求值过程:下标运算符优先级高于赋值运算符,故 list[2] 先求值。operator[] 返回 list.m_list[2] 的引用,因此表达式变为 list.m_list[2] = 3,完成整数赋值。

中,我们知赋值语句左侧必须为左值(具内存地址的变量)。operator[] 可用于赋值左侧,故其返回值须为左值。引用恒为左值,因其只能绑定到有地址的变量。因此返回引用即可满足要求。

若 operator[] 按值返回整数,则 list[2] 返回 m_list[2] 的值(如 6),表达式变为 6 = 3,非法!编译器报错:

error C2106: '=' : 左操作数必须为左值

const 对象重载 operator[]

上例中 operator[] 非 const,可用于修改非 const 对象状态。若 IntList 对象为 const,则无法调用非 const 版本,否则可能修改 const 对象。

可分别定义 const 与非 const 版本:非 const 版本用于非 const 对象,const 版本用于 const 对象。

#include <iostream>

class IntList
{
private:
    int m_list[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // 初始状态

public:
    // 非 const 版本:可用于赋值
    int& operator[] (int index)
    {
        return m_list[index];
    }

    // const 版本:仅用于读取
    // 若类型拷贝开销小,亦可按值返回
    const int& operator[] (int index) const
    {
        return m_list[index];
    }
};

int main()
{
    IntList list{};
    list[2] = 3; // 合法:调用非 const 版本
    std::cout << list[2] << '\n';

    const IntList clist{};
    // clist[2] = 3; // 编译错误:clist[2] 返回 const 引用,不可赋值
    std::cout << clist[2] << '\n';

    return 0;
}

消除 const 与非 const 重载间的重复代码

上例中,int& IntList::operator[](int)const int& IntList::operator[](int) const 实现完全一致,仅返回类型不同。若实现简单(单条语句),少量冗余可接受。

若实现复杂(如需验证索引合法性),重复代码则成问题。可将 const 版本实现逻辑集中,非 const 版本调用 const 版本并使用 const_cast 移除 const:

#include <iostream>
#include <utility> // for std::as_const

class IntList
{
private:
    int m_list[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

public:
    int& operator[] (int index)
    {
        // 使用 std::as_const 获取 const 引用,调用 const 版本
        // 再用 const_cast 移除返回引用的 const
        return const_cast<int&>(std::as_const(*this)[index]);
    }

    const int& operator[] (int index) const
    {
        return m_list[index];
    }
};

通常应避免用 const_cast 移除 const,但此处可接受:若调用非 const 重载,说明操作对象非 const,移除 const 安全。

C++23 进阶写法(供高级读者)

C++23 可用显式对象参数与 auto&& 区分 const 与非 const:

#include <iostream>

class IntList
{
private:
    int m_list[10]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

public:
    auto&& operator[](this auto&& self, int index)
    {
        // 复杂逻辑可放于此
        return self.m_list[index];
    }
};

检测索引有效性

重载下标运算符的另一优势是可增强安全性。直接访问数组时,下标运算符不检查索引合法性:

int list[5]{};
list[7] = 3; // 索引 7 越界!

若已知数组大小,可在重载函数中校验索引:

#include <cassert>   // for assert()
#include <iterator>  // for std::size()

class IntList
{
private:
    int m_list[10]{};

public:
    int& operator[] (int index)
    {
        assert(index >= 0 && static_cast<std::size_t>(index) < std::size(m_list));
        return m_list[index];
    }
};

上例使用 assert()<cassert> 提供)验证索引。若条件为假,程序终止并打印错误信息,远优于内存损坏。

若不想使用 assert(其在非调试构建中将被移除),可用 if 与自定义错误处理(如抛异常、调用 std::exit 等):

#include <iterator> // for std::size()

class IntList
{
private:
    int m_list[10]{};

public:
    int& operator[] (int index)
    {
        if (!(index >= 0 && static_cast<std::size_t>(index) < std::size(m_list)))
        {
            // 处理无效索引
        }
        return m_list[index];
    }
};

对象指针与重载 operator[] 不兼容

对对象指针调用 operator[] 时,C++ 会认为你在索引该类型对象数组:

#include <cassert>
#include <iterator>

class IntList
{
private:
    int m_list[10]{};

public:
    int& operator[] (int index)
    {
        return m_list[index];
    }
};

int main()
{
    IntList* list{ new IntList{} };
    list[2] = 3; // 错误:编译器认为访问 IntList 数组索引 2
    delete list;
    return 0;
}

由于无法将整数赋给 IntList,此代码无法编译;若赋值合法,则行为未定义。

规则 勿对对象指针调用重载 operator[]

正确写法为先解引用指针(注意括号,因 [] 优先级高于 *),再调用 operator[]

int main()
{
    IntList* list{ new IntList{} };
    (*list)[2] = 3; // 先获取对象,再调用重载 operator[]
    delete list;
    return 0;
}

此写法丑陋且易错,若无必要,避免使用指向对象的指针。

形参不限于整型

如前所述,C++ 将方括号内内容作为实参传递。通常此值为整数,但并非必须——你可令重载 operator[] 接受任意类型:double、std::string 等。

示例(仅为演示):

#include <iostream>
#include <string_view> // C++17

class Stupid
{
private:
    // 空
public:
    void operator[] (std::string_view index);
};

// 重载 operator[] 打印内容虽无意义,但可展示非整型参数
void Stupid::operator[] (std::string_view index)
{
    std::cout << index;
}

int main()
{
    Stupid stupid{};
    stupid["Hello, world!"];
    return 0;
}

输出:

Hello, world!

operator[] 接受 std::string 参数,可在以单词为索引的类中发挥作用。


测验

问题 1
Map 是一种以键值对存储元素的类。键必须唯一,用于访问对应值。本测验将编写一个简单 Map 类,实现按姓名为学生赋分。姓名作为键,分数(char 类型)作为值。

a) 首先,编写名为 StudentGrade 的结构体,包含学生姓名(std::string)和分数(char)。

b) 添加 GradeMap 类,内含名为 m_map 的 std::vector

c) 为该类重载 operator[]。函数接受 std::string 参数,返回 char 引用。函数体内先检查学生是否存在(可用 <algorithm> 中的 std::find_if)。若存在,返回分数引用即可;否则使用 std::vector::emplace_back() 或 push_back() 添加新 StudentGrade。添加后,用 std::vector::back() 获取刚加入的学生并返回其分数引用。

以下程序应能运行:

#include <iostream>
// ...

int main()
{
    GradeMap grades{};

    grades["Joe"] = 'A';
    grades["Frank"] = 'B';

    std::cout << "Joe has a grade of " << grades["Joe"] << '\n';
    std::cout << "Frank has a grade of " << grades["Frank"] << '\n';

    return 0;
}

问题 2
加分题 1:上述 GradeMap 类及示例程序存在多种低效原因。请描述一种改进 GradeMap 类的方法。

问题 3
加分题 2:为何以下程序可能不按预期运行?

#include <iostream>

int main()
{
    GradeMap grades{};

    char& gradeJoe{ grades["Joe"] };    // 触发 emplace_back
    gradeJoe = 'A';

    char& gradeFrank{ grades["Frank"] }; // 触发 emplace_back
    gradeFrank = 'B';

    std::cout << "Joe has a grade of " << gradeJoe << '\n';
    std::cout << "Frank has a grade of " << gradeFrank << '\n';

    return 0;
}

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"