Reading Note of CppPrimer-Chapter14

Overloaded Operations and Conversions

Overload Operators

Overview

  • 重载的运算符是具有特殊名字的函数,他们的名字由关键字operator 和其后要重载的运算符号共同构成,和其他函数一样,重载的运算符也包含返回值参数列表函数体

    我们只能重载已有的运算符,而无权发明新的运算符;对于一个重载的运算符来说,其优先级和结合律应该与对应的内置运算符保持一致

    对于一个运算符函数来说,他或者是类成员函数,或者至少含有一个类类型的参数;即无法对内置类型进行运算符重载

    1
    int operator+(int, int){}          // invalid,没有类类型的参数,无法对内置类型重载
  • 重载运算符的参数

    一般来说,重载运算符函数的参数数量该运算符作用的运算对象的数量一样多

    除了函数调用运算符 operator() 之外,其他重载运算符函数不能有默认实参

    如果一个重载运算符是类成员函数,则他的第一个 (即左侧的) 运算对象隐式绑定到 this 指针上面,即成员运算符函数的显式参数比实际调用时运算对象的参数少一个

    当我们将运算符定义为成员函数的时候,他的左侧运算对象必须是运算符所属类的一个对象

    1
    2
    3
    std::string s = "string";
    std::string t = s + " Hi"; // valid
    std::string u = "Hi " + s; // invalid,等价于"Hi".operator+(s),但是const char* 没有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
    class A{
    public:
    A(int a_):a(a_){}
    // compared to operator-, the left-hand params is implicitly binded to this
    A operator+(const A& rhs){
    A ret = *this;
    ret.a+= rhs.a;
    return ret;
    }
    int a = 0;
    };

    // overload operator funcion, num of arguments is the same as parameters
    A operator-(const A& lhs, const A& rhs){
    std::cout << lhs.a << " " << rhs.a << std::endl;
    A ret = lhs;
    ret.a -= rhs.a;
    return ret;
    }

    A a1(10);
    A a2(20);
    // call operator+
    A a3 = a1+a2;
    std::cout << "a3: " << a3.a << std::endl;
    // call operator-
    A a4 = a1 - a2;
    std::cout << "a4: " << a4.a << std::endl;
  • 重载运输符的调用

    通常运算符作用于类型正确的实参,是以间接的方式调用重载的运算符函数;等价于像调用普通函数一样直接调用运算符函数;类成员函数同理;

    1
    2
    3
    data = data1 + data 2;
    data = operator+(data1, data2); // 调用等价
    data = data1.operator+(data2); // 调用等价, 类成员函数
  • 重载运算符的返回值类型

    • 逻辑运算符关系运算符返回 bool 类型
    • 算术运算符应该返回类类型的值
    • 赋值运算符复合赋值运算符应该返回左侧运算对象的引用*this
    • 输入输出运算符应该返回 streamnon-const reference
    • 下标运算符应该返回引用类型
    • 前置自增自减运算符应该返回引用类型后置自增自减运算符返回非引用类型
    • 解引用运算符应该返回引用类型
    • 箭头运算符应该返回指针类型或者定义了箭头运算符的类的对象
  • 运算符重载可以是成员函数也可以是非成员函数;下面的准则帮助我们决定他的形式:

    1. 赋值 =、下标 []、调用 ()、成员访问 ->必须是成员函数

    2. 复合赋值 (+=, -=, *=, /=) 一般来说是成员函数

    3. 改变运算状态的,或者与给定类型密切相关的运算符,如递增 ++、递减 --、解引用 *需要是成员函数

    4. 具有对称性的运算符,如算术 (+-*/),关系 (<<===>=>) 和运算符 (<<>>) 通常应该是非成员函数

      重载输出运算符只能是非成员函数,否则左边的第一个 ostream 的参数就是类本身了,除了这个类是 iostream 之外都不合法;

      通常情况下我们会把算术运算符和逻辑运算符定义为非成员函数允许多左侧或者右侧的对象进行转换

  • 某些运算符指定了运算对象的求值顺序,而重载的时候无法体现出这种顺序,所以不建议重载

    逗号运算符,取地址运算符& 已经有了内置的特殊含义,不建议重载

    &, |, ,, ?:, &&, ||

Input Output Operators

  • 重载输出运算符 operator>>

    重载输出运算符第一个形参是 ostream类型的非常量的引用 (non-const reference);第二个形参一般为要打印对象的常量引用 (const reference);返回的是 ostream引用

    内置类型的 << 函数一般没有格式化符号 (如 \n),是为了便于用户的格式控制,重载的 << 一般也应该没有格式化符号

    1
    2
    3
    4
    5
    ostream &operator<<(std::ostream& os, const Sales_item& item){
    os << item.isbn() << " " << item.units_sold << " "
    << item.revenue << " " << item.agv_price()/* << std::endl/*; // should not include format syntax
    return os;
    }
  • 重载输入运算符 operator<<

    重载输入运算符第一个形参是运算符将要读取的 istream 类型流的非常量引用 (non-const reference);第二个形参是将要读入到的对象的非常量引用 (non-const reference);返回值是 istream 的引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    istream &operator>>(std::istream& is, Sales_data& item){
    double price;
    is >> item.bookNo >> item.unit_sold >> price;
    if (is){ // check whether stream is still valid
    item.revenue = item.unit_sold * price;
    }
    else{
    item = Sales_data();
    }
    return is;
    }
  • 错误处理

    输入运算符需要处理可能失败的情况,输入倒不必;

    当读取操作失败时,输入运算符应该负责从错误中恢复;导致输入失败一般包含输入的类型错误或者抵达文件结尾等;

Arithmetic and Relational Operators

  • 算术运算符 +-*/

    算术运算符通常会计算他的两个运算对象,然后得到一个新值,这个值有别于任何一个运算对象,常常是函数体内定义的一个新的局部变量;操作完成返回这个局部变量的副本作为返回值

    1
    2
    3
    4
    5
    Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs){
    Sales_data sum = lhs;
    sum += rhs;
    return sum;
    }
  • 相等运算符 operator==

    相等运算符会比较对象的每一个数据成员,当所有的成员都相等的时候,才认为两个对象相等;通常应该是非成员函数

    1
    2
    3
    4
    Sales_data operator==(const Sales_data &lhs, const Sales_data &rhs);
    Sales_data operator!=(const Sales_data &lhs, const Sales_data &rhs){
    return !(lhs == rhs);
    }
  • 关系运算符 <<===>=>

    理论上定义了 < 运算符,其他的运算符都可以由 < 推导出来;通常应该是非成员函数

    关系运算符< 的定义不得和相等运算符==定义冲突

    <(a, b) : (a < b)
    <=(a, b) : !(b < a)
    ==(a, b) : !(a < b) && !(b < a)
    !=(a, b) : (a < b) || (b < a)

    >(a, b) : (b < a)
    >=(a, b) : !(a < b)

Assignment Operators

  • 赋值运算符 operator=

    对于相同类型对象,类本身是可以通过拷贝赋值和移动赋值来实现的赋值操作;对于不同类型对象我们可以通过重载来实现用作为右侧运算符;

    赋值运算符返回左边对象的引用;和其他赋值运算符一样,必须先释放当前的内存空间,再创建一片新空间;

    而重载的赋值运算符,不论形参类型是什么,赋值运算符都必须定义为成员函数

    1
    2
    3
    4
    5
    6
    7
    8
    // copy assignment function
    String& String::operator=(const String &ths){
    return *this;
    }
    // overload to support initializer_list
    String& String::operator=(const std::initializer_list<String> ths){
    return *this;
    }
  • 复合赋值运算符 +=, -=, *=, /=

    为了与赋值运算符保持一致,复合赋值运算符也要返回左侧对象的引用;也倾向于定义为类的成员函数

    1
    2
    3
    4
    // left-hand operand implicitly binds to this pointer
    String& String::operator+=(const String rhs){
    return *this; // return reference of lhs
    }

Subscript Operators

  • 下标运算符 operator[]

    为了与 built-in 下标运算符 [] 定义兼容,下标运算符应该返回引用,这样的好处是下标运算符可以出现在赋值运算符的任何一端

    同时,下标运算符应该有常量版本非常量版本,一个返回普通引用,一个返回常量引用;下标运算符必须是成员函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class StrVec{
    public:
    std::string& operator[](std::size_t n){
    return elements[n];
    }
    // Caution: const StrVec should return const reference
    const std::string& operator[](std::size_t n) const{
    return elements[n];
    }
    private:
    std::string *elements; // string array
    };

Increment and Decrement Operators

  • 递增 operator++/ 递减 operator-- 运算符

    递增 / 递减运算符一般定义为成员函数,分为前置后置版本,一般会同时实现前置和后置版本;

    为了解决函数重载不能根据前置后置版本的返回值不同来区分前置后置的问题,规定后置版本接受一个不被使用的额外的 int 类型的参数,当使用后置版本的时候,编译器自动为这个形参提供一个值为 0 的实参;

    1
    2
    p.operator++(0);    // 后置版本
    p.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
    class StrBlobPtr{
    public:
    StrBlobPtr& operator++(){ // 前置++
    // ++
    return *this;
    }
    StrBlobPtr& operator--(){ // 前置--
    // --
    return *this;
    }
    StrBlobPtr operator++(int){ // 后置++,由于无法通过返回值实现重载,故后置版本多了一个不被使用的int参数以重载
    StrBlobPtr ret = *this; // 当我们使用后值版本的时候,编译器为这个形参提供一个值为0的实参
    // ++
    return ret;
    }
    StrBlobPtr operator--(int){ // 后置--
    StrBlobPtr ret = *this;
    // --
    return ret;
    }
    };

    // invoke
    // if p is define by StrBlobPtr p();, then p is a function pointer, which is not we expect
    StrBlobPtr p;
    p++; // 后置版本
    ++p; // 前置版本

Member Access Operators

  • 解引用运算符 operator*

    解引用运算符 *返回一个引用,解引用通常必须是类的成员函数

    1
    2
    3
    4
    5
    6
    7
    class StrBlobPtr{
    public:
    std::string& operator*() const{
    // check whether curr is out of range
    return (*p)[curr];
    }
    };
  • 箭头运算符 operator->

    箭头运算符 -> 本身不执行任何操作,而是调用解引用符并返回解引用的结果;箭头运算符必须是类的成员函数

    箭头运算符应该返回指针类型或者定义了箭头运算符的类的对象

    1
    2
    3
    4
    5
    6
    class StrBlobPtr{
    public:
    std::string* operator->() const{
    return & this->operator*();
    }
    };

    重载箭头运算符的时候,永远不能丢掉成员访问这个最基本的含义;我们可以改变的是箭头运算符从哪种类型的对象当中获取成员,而箭头运算符获取成员这事实则永远不变;对于 p->mem 这样的表达式来说,根据 p 的类型不同,可以有不同的解析:

    1
    2
    (*p).mem;             // p is built-in pointer
    p.operator->()->mem; // p is an class object

Function Call Operators

  • 函数对象

    如果类重载了函数调用运算符 (),则我们也可以像使用函数一样去使用该类的对象;这样的类还可以储存状态,比函数会更灵活

    如果类定义了调用运算符,我们称这样的类为函数对象;函数对象常常作为范型算法的实参;

  • 函数调用运算符 operator()

    函数调用运算符必须是成员函数;一个类可以定义多个重载的函数调用运算符,每个重载函数可以有 0 个或多个参数;

    1
    2
    3
    4
    5
    6
    7
    8
    struct ShorterString{
    bool operator()(const std::string& s1, std::string& s1){
    return s1.size() < s2.size();
    }
    };

    // use the function object, create a ShorterString object as third parameter
    std::sort(words.begin(), words.end(), ShorterString());
  • lambda 表达式本质上就是一个函数对象。在我们写了一个 lambda 之后,编译器将该表达式翻译成一个未命名类的未命名对象,在 lambda 表达式产生的类中,只有一个重载函数调用运算符

    • 通过值捕获的 lambda 表达式产生的类,拷贝的值会成为生成的类的构造函数;
    • 通过引用捕获的 lambda 表达式产生的类,编译器可以直接使用该引用。参见 chapter10

    lambda 表达式产生的类不含默认的构造函数、赋值函数以及析构函数;是否含有默认的拷贝 / 移动构造函数视其捕获的对象类型而定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // ShorterString can be viewed as a generated class from a lambda expression
    auto shorter_strin = [](const std::string& s1, std::string& s1){return s1.size() < s2.size();}

    // equal to
    struct ShorterString{
    bool opperator()(const std::string& s1, const std::string& s2) const {
    return s1.size() < s2.size();
    }
    };
  • 标准库里面定义了一组表示算术运算符、关系运算符、逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <functional>
    std::plus<T>; // +
    std::minus<T>; // -
    std::multiplies<T>; // *
    std::divides<T>; // /
    std::modules<T>; // %
    std::negate<T>; // -, negative
    std::equal_to<T>; // ==
    std::not_equal_to<T>; // !=
    std::greater<T>; // >
    std::greater_equal<T>;// >=
    std::less<T>; // <
    std::less_equal<T>; // <=
    std::logic_and<T>; // &&
    std::logic_or<T>; // ||
    std::logic_not<T>; // !

    标准库规定这些对象对指针对象同样适用,可以解决无法直接比较指针的尴尬境地;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    std::vector<std::string *> nameTable;

    // invalid, compare two irelevant pointer results in undefine result
    std::sort(nameTable.begin(), nameTable.end(), [](std::string *lhs, std::string *rhs));

    // valid to compare memory address
    std::sort(nameTable.begin(), nameTable.end(), std::greater<std::string*>());

    std::greater<T> // template
    std::greater<std::string *> // class
    std::greater<std::string *>() // class defaule constructor
    std::greater<std::string *>()() // call func-call operator of the object
  • c++ 中可调用对象的类型:

    1. 函数
    2. 函数指针
    3. lambda 表达式
    4. std::bind 创建的对象
    5. 重载函数调用符号的类(函数对象)
  • std::function

    可调用对象有类型,比如 lambda、函数指针等;不同的可调用对象可能有相同的调用形式 (call signature),比如同样是 int (int, int) 这个调用形式,实际的可调用对象可能是对两个 int 相加、相减亦或是相乘相除;

    可以使用 std::function将各种不同的调用对象类型转换为相同的类型

    std::function 是一个模版,模版参数是调用形式,调用形式和函数类型相同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <functional>
    int add(int, int);
    struct divide{
    int operator()(int, int);
    };
    std::function<int(int,int)> f1 = add; // 函数指针
    std::function<int(int,int)> f2 = divide(); // 函数对象类对象
    std::function<int(int,int)> f3 = [](int a, int b){return a-b;}; // lambda表达式
    std::map<std::stringm std::function<int(int,int)>> func_map = {
    {"add": f1},{"division": f2},{"substration": f3}};
  • 不能将重载函数的名字赋值给 std::function 类型的对象,解决办法是转换为函数指针或者 lambda

    1
    2
    3
    4
    int add(int, int);
    Sales_data add(const Sales_data&, const Sales_data);

    std::function<int(int, int)> func = add; // ambiguous add function
    1
    2
    error: no viable conversion from '<overloaded function type>' to 'std::function<int (int, int)>'
    std::function<int(int, int)> func = add; // ambiguous add function
    1
    2
    3
    4
    5
    6
    // solution 1: func pointer
    int (*add_func)(int, int) = add;
    std::function<int(int, int)> func = add_func;

    // solution 2: lambda
    std::function<int(int, int)> func = [](int a, int b){return add(a, b);}

Overloading and Conversions

  • 类类型转换一般可以通过两种方式实现:

    1. 只接受单独一个实参的非显示构造函数,比如隐式地将 const char* 转换为 std::string
    2. 类型转换运算符
  • 类型转换运算符 (conversion operator)

    可以给类添加一种特殊的成员函数,他负责将一个类类型的值转换为其他类型:类型转换运算符 (conversion operator); 比如 iostream 就有转化为 bool 的操作符来方便用户判断 stream 是不是可用;

    1
    operator type() const;
    1. type 表示某种类型,可以是任何可以作为函数返回值的类型(void 除外),因此可以返回引用和指针,但是不能返回数组和函数
    2. 类型转换运算符没有显式的返回类型,也没有形参,必须是类的成员函数
    3. 类型转换符隐式的执行,所以无法给这个函数传递实参;
    4. 类型转换函数不负责指定返回类型,但是实际上每个类型转换函数都会返回一个对应类型的值;
    5. 类型转换函数不应该改变类的内容,需要使用 const 修饰;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class SmallInt;
    operator int(SmallInt&); // invalid, not a member function
    class SmallInt{
    public:
    operator int() const; // valid
    int operator int() const; // invalid, a return value should not be specified
    operator int(int=0) const; // invalid, parameter list should be empry
    operator int*() const; // valid, cast to int*
    };
    // usage
    SmallInt s;
    s = 4; // convert int to smallint by constructor implicit conversion, like const char* to std::string
    s + 3; // smallInt no plus operator, so s will be implecit converted to an int, and then plus 3
  • 显式类型转换运算符 (explicit conversion operator)

    为了防止类型转换隐式的转换而产生意外的结果,c++11 引入了显式类型转换运算符 (explicit conversion operator);当类型转化为显式的时候,我们也可以执行显示的强制类型转换static_cast 来做类型转换

    如果表达式作为条件的时候,对于用户来说,显示转换的要求会被忽略;编译器会自动加入显示的转换,因此 operator bool() 通常定义为 explicit 也是 OK 的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class SmallInt {
    public:
    explicit operator int() const{
    // should return an int
    }
    explicit operator bool() const{
    // should return an bool
    }
    };

    // usage
    SmallInt s= 3;
    //s + 3; // invalid, cannot convert to int implicitly
    static_cast<int>(s) + 3; // valid, explicit conversion
    if (s){ // valid, explicitly converted by compiler when used as condition
    }
  • 除了显式的向 bool 类型转换之外,我们应该尽量避免定义类型转换函数;根据函数实参转换优先级,决定函数匹配的顺序 (参见 chapter 6) 避免会有二义性的类型转换函数;

  • 和普通的命名函数不同,重载的运算符的候选函数集包含了内置版本函数也包含普通非成员函数,如果左侧运算符是类,则候选函数还可以是类的成员函数(因为有些操作符函数既可以在类里面定义,也可以在类外定义);

    当我们通过类类型的对象进行函数调用时,只考虑该类的成员函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class A{
    public:
    A(int a_):a(a_){}
    A operator+(const A& rhs){
    std::cout << "-----------" << std::endl;
    A ret = *this;
    ret.a+= rhs.a;
    return ret;
    }
    int a = 0;
    };

    A operator+(const A& lhs, const A& rhs){
    std::cout << "##########" << std::endl;
    std::cout << lhs.a << " " << rhs.a << std::endl;
    A ret = lhs;
    ret.a -= rhs.a;
    return ret;
    }

    A a1(10);
    A a2(20);
    A a3 = a1+a2; // call class member function,
    // print -----------