5.运算符重载、模板(TBC)

  2021-9-10 


运算符重载

  1. 对于成员函数的运算符重载,它的左侧运算对象必须是运算符所属类的一个对象

    这时,表达式右边第一个元素是被调用对象,第二个元素是运算符重载函数传入的参数

    string t = obj + "!";
    #相当于调用 obj.operator+("!")

    所以第一个元素必须是运算符重载函数所属类的对象

    当一个重载的运算符是成员函数的时候,this绑定到左侧运算对象。成员运算符函数的显式参数数量比运算对象的数量少一个。

  2. 对于非成员函数的运算符重载,要求至少有一个运算对象是类类型(类指针类型也不行)。

    比如如果想重载int operator(const int a,const int b)是错误的!

    cout<<a;就是一个典型的非成员函数运算符重载函数的调用,输入输出运算符必须是非成员函数重载。cout是第一个元素,类型位ostream&,第二个元素是a,运算符<<,且该表达式的返回值为一个ostream&。然后在ios库中有定义ostreamvoid*类型的转换,于是就可以将其作为while等的判断条件,如果调用出错(如cin读到行尾等),那么ostream&转换成void*为0,即False,那么循环就会跳出。(附,ostream库中转换运算符定义:operator void *() const { if(state&(badbit|failbit) ) return 0; return (void this; }

    注意ostream&要设置为非常量,因为ostream输出数据会改变其状态。

  3. 类型转换运算符语法特殊

    operator type() const; 转换为type()类型,返回转换后的type()类型的结果explicit可以禁止隐式转换,只能显式转换。注意定义类型转换运算符这个格式比较特殊

    注意,在表达式作为if/while/for/!/||/&&/?:等语句的条件部分的时候,显式的类型转换将会被隐式地执行(即使有explicit也会隐式执行显示转换)。所以如果定义了到bool类型的转换,就可以将该语句作为条件来使用。

    注意,要避免具有二义性的类型转换。二义性主要是隐式转换存在多种优先级相同的转换路径,导致编译器不在做隐式转换的时候不知道该选择哪一种。比如:两个类AB中A的构造函数与B的类型转换运算符二义;两个类型转换运算符都不能精准匹配导致的二义;几个重载函数的参数分属不同类型,但这几个类恰好定义了同样的转换构造函数(这种情况即使精准匹配也会二义,优先级视为相同);类与算数类型且既提供了目标是算数类型的类型转换、也提供了重载运算符的算数表达式等等。为了避免二义性的建议:不要令两个类指向相同的类型转换;避免转换目标是内置算术类型的类型转换。除了显式地向bool类型转换,我们应该尽量避免定义类型转换函数并尽可能限制那些“显然正确”的非显式构造函数。禁用隐式转换

  4. 为了使用后置运算符,后置版本接受一个额外的(不被使用)int类型的形参。当我们使用后置运算,编译器为这个形参提供一个值为0的实参,来调用这个后置版本重载。eg:

    Obj Obj::operator++; //前值版本
    Obj Obj::operator++(int); //后值版本
  5. 箭头运算符的重载也属于特殊一类

    重载的箭头运算符必须返回指向类的指针或者自定义了箭头运算符的某个类的对象然后编译器会再调用内置箭头运算符访问成员。

    运用箭头运算符的意义就是:重新定义内置箭头运算符应该访问哪个类的成员,而这个类就是重载的箭头运算符的返回值(指向它的指针)

    对于point->action(),由于优先级规则,它实际等价于编写(point->action)()

    ①如果point是一个指针,指向具有名为action的成员的类对象,则编译器将代码编译为调用该对象的 action成员(默认语义)。

    ②否则,如果point是定义了 operator->操作符的类的一个对象,point->actionpoint.operator->()->action 相同。即,执行point 的 operator->(),然后使用该结果重复这三步。(递归调用)

    ③否则,代码出错。

    总结:通过箭头->操作符的执行过程,我们可以得到结论“重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。”返回前者用于执行编译器默认语义终结箭头运算符的调用(如果重载的箭头运算符返回类型是指针,则内置箭头操作符将作用于该指针,编译器对该指针解引用并从结果对象获取指定成员),返回后者用于递归调用。(如果返回类型是类类型的其他对象(或是这种对象的引用),则将递归应用该操作符。编译器检查返回对象所属类型是否具有成员箭头,如果有,就应用那个操作符;否则,编译器产生一个错误。这个过程继续下去,直到返回一个指向带有指定成员的的对象的指针,或者返回某些其他值,在后一种情况下,代码出错。)
    (参考:https://blog.csdn.net/dfwseq/article/details/25691063)

    重载的箭头运算符中通常将工作委托给*解引用运算符(获取实体),再返回指向该实体的指针,以使得语义自然

  6. 重载运算符的定义不应该脱离这个运算符最基本的含义、

  7. 类型类型

    对于函数 int func(int a,int b),其类型为int(int,int)

    对于函数指针int *func(int a,int b),其类型为int (*)(int,int)

    注意这里函数指针很容易搞错,(*)其实是(*func)的类型定义形式(即函数指针),前面的函数返回类型和后面的函数参数表都是func本身的定义,(*)是关键,跟在func后面表明这个类型是这个func的指针。

    C++中,直接使用函数名、函数对象名、数组名都是指针!

  8. 函数调用运算符的格式要注意

    ovid operator()(const string str) const{}其中的operator()就是定义重载()运算符,也就是函数调用。第二个括号才是参数表!

    函数调用运算符可以做到像函数一样使用对象,这种对象称为函数对象,也叫仿函数,是非匿名的函数对象

    函数对象常常作为泛型算法的实参,泛型算法负责调用函数对象。

    标准库也定义了一些函数对象,比如greater<Type>less<Type>等,可以很方便用作泛型算法的实参

  9. 可调用对象:①函数 ②函数指针 ③lambda表达式 ④bind创建的对象 ⑤重载了函数调用运算符的类

    通过传递函数指针(或函数对象指针),可以将函数以参数的方式进行传递,方便灵活调用。(注意:函数本身并不能作为参数传递,所以我们传递函数指针,定义的类型也是函数指针类型;但lambda表达式本身可以作为参数传递,可以用一个auto类型接收)

    lambda表达式也是(匿名)函数对象,与非匿名函数对象可以互相替代。但是lambda表达式的类型很难表达

    为了方便表达可调用对象类型,C++11引入function类型,用函数调用形式来定义可调用对象,以便不写类型只要调用形式相同(任何可调用对象,无论是函数、函数指针、lambda、函数对象),都可以用同一个function模板来表示一个函数类型。比如:function<int(int,int)>就代表函数调用形式为int(int,int)的可调用对象类型,注意这里省略了变量名

    eg:

    class a
    {
    	void operator()(const string &str)
        {
            cout<<str<<endl;
        }  
    };
    void func(const string &str)
    {
        cout<<str<<endl;
    }
    int main()
    {
    	//不用function,直接手动定义函数类型
        vector<void(*)(const string&)> vec;  //函数名是指针类型!
        vec.push_back(func);
        vec[0]("hello world"); //hello world
    
        //定义function,只要满足调用形式是void(const string&)的都可以作为vector元素
        vector<function<void(const string&)>> vec2;
        
        vec2.push_back(func);  //函数指针
        auto func2 = [](const string& str){cout<<str<<endl;};  //lambda
        vec2.push_back(func2);
        vec2.push_back(a);  //函数对象
        vec2[0]("hello world2"); //hello world2
        vec2[1]("hello world3"); //hello world3
        vec2[2]("hello world4"); //hello world4
    }

    这样,在使用可调用对象作为成员的时候就方便了,比如在初始化模板定义的时候(如定义容器),不能用auto,就必须得用function类型来表达lambda类型了

    ATT:lambda表达式产生的类不含默认构造函数,赋值运算符,和默认析构函数,是否有默认的拷贝构造函数通常视捕获的数据成员类型而定。

#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Obj
{
    //make function friend ahead of definination!
    friend ostream& operator<<(const ostream&, const Obj&);
public:
    //const can only be initialized by initialize list
    Obj(const int a, const int b):a(a),b(b) {}
    int operator+(const double a)
    {
        return 4;
    }
    // no pram but () should exist
    Obj& operator++()
    {
        ++b;
        return *this;
    }
    Obj& operator++(int)
    {
        b++;
        return *this;
    }
    int operator[](const int index) const
    {
        return vec[index];
    }
    //const function cannot return a non-const reference
    const vector<int>& operator*() const
    {
        return vec;
    }
    //special format
    //'->' operator must return a pointer or a object that defined the '->' operator
    const vector<int>* operator->() const
    {
        return & this->operator*();
    }
    //special format
    // format special :no return define in function defination
    explicit operator int() const
    {
        return 10;
    }
    //special format
    void operator()(const string str) const
    {
        cout<<str<<endl;
    }

    const int a;
    int b;
    vector<int> vec = {0,1,2,3,4,5};
};
ostream& operator<<(ostream& os, const Obj& obj)
{
    cout<<obj.b;
    return os;
}
int main()
{
    Obj a(3, 4);
    ++a;
    cout<<a++<<endl;
    cout<<static_cast<int>(a)<<endl;
    cout<<(*a)[1]<<endl;
    cout<<(a->size())<<endl;
    a("object function");
}

模板

  1. 模板参数推断

    模板参数作为函数类型参数的一部分,模板推断出来形参缺啥模板参数就补啥,比如模板函数形参类型是const T&,若实参为string*,则模板推断出来模板参数Tstring*,形参为const string* &,是一个指向字符串指针的常量引用。

    只有调用模板函数不显式指定模板实参(类型,比如<int>)的时候,即直接调用模板函数的时候,才会触发模板实参推断。此时模板会通过调用的时候传入的实参自动推断类型,来实例化形参类型

    可以部分显式指定模板实参,这样会执行部分模板参数推断。注意模板参数推断只能推断第一个显式指定模板参数之后的模板参数,之前的不能推断。

    typename <typename T1,typename T2,typename T3>
    T1 sum(T2 a,T3 b) { return T2+T3; }
    
    sum(1,2);  //完全模板参数推断
    sum<int,int,int>(1,2); //显式指定参数,不触发模板参数推断
    sum<int>(1,2); //部分显示指定参数T1,而T2,T3则触发模板参数推断

    注意,模板实参推断是模板实例化的时候才会发生的事情。

  2. 模板隐式实例化与显式实例化

    模板实例化:编译器通过模板生成具体类或函数代码。类模板的名字不是类型,类模板用来实例化类型。

    • 隐式实例化,只有模板被使用的时候才会实例化如果一个模板类的成员函数没有被使用,则它不会被实例化(生成代码)。成员函数只有被使用到时才进行实例化,这一特性使得即使某种类不能完全符合模板要求,我们仍然能用该类型实例化类。举例:

      template <typename T> struct Obj
      {
          T a;
          void func()
          {
              a = "abc";
          }
      };
      int main()
      {
          Obj<int> obj;  //不会报错
          obj.func();  //使用函数func了,生成成员函数func的代码,报编译错误
      }
      
      /****显示实例化,则所有函数都会实例化****/
      int main()
      {
          template struct Obj<int>; //执行显示实例化,所有成员函数代码都生成,报编译错误
      }
    • 隐式实例化一使用就会被实例化可能会导致,相同的实例可能出现在多个对象文件中,实例化多个相同模板造成严重的开销。可以通过显式实例化来避免:

      ①在需要用到模板的地方声明extern template declaration;,编译器遇到extern函数就不会在本文件中生成这个实例化版本的代码

      ②在想要生成实例化版本的地方,不用extern,直接使用template declaration;定义(不需要内容),就会显示初始化这个实例化版本。

      注意,上面两者其实是分开的,extern可以实现跳过定义,而后面 template declaration;就是实例化版本的定义语句

      注意:这个定义语句是定义一个具体的实例化版本,即给出实参

  3. 类模板与友元

    如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例(显然,因为每个实例都有该友元声明代码)。如果友元自身是模板,类可以授权给所有友元实例,也可以只授权给特定实例。

  4. 模板函数重载

    template<typename T>
    void fun(T a){ /*do sth*/}
    
    template<typename T> void fun(T &&a) { /*do sth different*/}  //模板函数重载
    template<typename T1, typename T2> void fun(int a, T1 b, T2 c) { /*do sth different*/}  //模板函数重载2

    若多个重载模板提供同样好的匹配,则优先匹配最特例化的版本

  5. 模板特例化

    特例化的本质是直接特定实例化一个模板而非重载它。因此,特例化不影响函数匹配。

    template<typename T>
    void fun(T a){ /*do sth*/}
    
    template<> fun(int a){ /*do sth different*/ }  //模板特例化,并实例化
    template fun(int a); //显式实例化声明

    注意,无论特例化还是实例化声明都显式指定了模板参数,实际上应该在函数名/类名后跟<>,比如fun<int>(int a),这里只是简写了。如果产生了歧义,就不能简写【eg】。

    注意模板特例化和显式实例化的区别。语法上,模板特例化要带<>,且需要紧跟函数体/类体;而模板显式实例化声明不带<>,且不能跟函数体/类体。

    模板特例化是对一个模板的特定模板参数执行不同内容的方式;而显式实例化是触发模板实例化的手段,只是个申明。模板特例化的时候也实例化了。

    模板实例化优先级:非模板函数>特例化模板>偏特化模板(仅类)>普通模板

    部分特例化简称偏特化

    我们可以偏特化类模板,不能偏特化函数模板。偏特化的类模板的模板参数列表是原始模板参数列表的一个子集或一个特例化版本。偏特化类模板仍是一个模板。(偏特化类模板其实就相当于模板函数重载的类版本,我们想要偏特化模板函数实际上用模板函数重载即可。)

    //原始模板类A,接收两个模板参数T1 T2
    template <typename T1,typename T2>
    struct A
    {
        T1 a;
        T2 b;
        A() { cout<<"2 arg ver"<<endl; }
    };
    //偏特化模板类A,特化第一个参数为int
    template <typename T2>
    struct A<int, T2>
    {
        int a;
        T2 b;
        A() { cout<<"1 arg ver"<<endl; }
    };
    
    int main()
    {
        A<float,float> a1; //调用原始模板类进行实例化
        A<int,float> a2;  //调用偏特化模板类A进行实例化
    }

    可以只特例化特定成员函数而不是整个模板。

    重载的模板函数还是模板函数,但是特例化模板是实例化!模板函数重载不提供实参,而模板特例化是要提供实参以实例化。

  6. shared_ptr在运行时绑定删除器,unique_ptr在编译时绑定删除器

    这是以前遇到的两个模板类,现在就可以看出删除器使用的区别了。

    shared_ptr<T>的删除器是作为函数的参数传入的,运行时绑定,灵活,可以更改删除器,但效率低

    unique_ptr<T,D>的删除器D是作为模板参数实例化unique_ptr模板类的,删除器D是实例化类的成员,属于编译时就将删除器生成为类代码一部分了,效率更高,但灵活性不足,不能更改删除器。

  7. 在模板类中,当我们希望通知编译器一个名字表示类型时,必须用typename关键字

  8. 注意将可调用对象作为模板参数的使用

    泛型算法compare就是这么使用函数对象的。

    template <typename T, typename F = less<T>>
    int compare(const T &v1, const T &v2, F f = F())
    {
        if (f(v1, v2)) return -1;
        if (f(v2, v1)) return 1;
        return 0;
    }

    首先,这里用到了默认模板实参,模板参数F如果不声明则为less<T>

    然后注意,这里的 模板实参F类型less<T>

    然后在函数形参表中调用默认构造器初始化一个类F的对象fF f = F()f可调用对象,作为模板函数的默认参数传入。

  9. 类型转换

    对于模板参数推断,将实参传递给带模板类型的函数型参时,能够自动应用的转换类型只有const转换及数据或函数到指针的转换

    对于模板类型参数已经显式指定了的函数实参,进行正常的类型转换。

  10. decltype与尾置返回类型

    有时候我们不知道返回类型是什么,尤其是如果模板参数T可能被实例化为另一个模板类(比如容器),且我们要返回容器元素的类型,那我们不可能在模板实例化之前知道这个类型是什么,所以可以用尾置返回类型,在实例化阶段获取具体类型。eg:

    auto get_first_inside(T item) -> decltype(*item)
    {
        return *item;
    }
    
    int main()
    {
        vector<int> a = {1};
        auto result = get_first_inside(a.begin());
    }

    注意:decltpye()是编译时类型推导,并没有用到RTTI机制!这里用decltype即可,不需要用到RTTI,因为模板实例化实际上就是生成代码,这是在编译期做的事情,而不是运行时!

  11. remove_reference


且听风吟