[cpp deep dive] `operator` deep dive

我在还没深入学习操作符重载的时候,虽然可以自己边debug边整出一个可以运行的操作符重载,但对其的了解却十分浅显,甚至连有些基本概念都搞不清楚.这大概就是传说中的半桶水.
我通过这篇文章已经解决了如下问题.

operator overloading函数在什么情况下会被调用?operator的操作数如何对应到函数参数?左操作数是用户自定义类?连续的operator如何处理?返回引用?编译器到底替我们生成了什么?

参考资料:

  • 整个chapter9 9.2 — Overloading the arithmetic operators using friend functions
  • c++ ref
  • return-value-of-operator-overloading
  • operator-overloading-member-function-vs-non-member-function
  • The Three Basic Rules of Operator Overloading in C++
  • what-are-all-the-member-functions-created-by-compiler

操作符重载分为三种:

It turns out that there are three different ways to overload operators:

  • the member function way
  • the friend function way
  • the normal function way

关于这三者,c++ ref上面有个表:

[cpp deep dive] `operator` deep dive_第1张图片
Paste_Image.png

需要注意的点:

  • the friend function way:

    • 一个类的friend函数不是这个成员,仅仅在这个类里做了权限声明;
    • friend函数可以访问参数里这个类里面的私有成员.
    • friend函数是根据参数类型被调用的,假设参数类型类似(int,myclass)和(myclass, int)这样,需要写两个friend函数以供两种情形(int @ myclass或myclass @ int)调用. 为了节省代码量,一种常用的实现是用其中一个调换下参数顺序去调用另一个,如Phase_1 .1的32~34行.
    • Q: This is slightly less efficient than implementing it directly (due to the extra function call).What if using inline function ?
      A: It depends on whether the compiler honors the inline request or not. It may or may not.

  • the normal function way: friend 函数增加了耦合,所以如果可能的话(例如该类本身提供获取私有数据的方法)使用普通操作符重载函数(即类中没有friend声明).

    • 普通的操作符重载函数仅仅是对私有数据的权限不同而已.
  • the member function way:

    • 通过上面的图可以看出左/右操作数与成员函数的调用关系:(这个图十分重要:为何感到半桶水,大多都是没有系统地了解过这些重载的分类与调用规则)
      • a@b(@除了赋值操作符=) -->调用a.operator@(b)operator@(a,b)
      • a@ --> 调用a.operator@(0)operator@(a,0)
      • @a --> 调用a.operator@()operator@(a)
      • 赋值运算符与取下标运算符只能通过成员函数的形式来重载.
    • 与friend函数区别的是:operator成员函数参数的数目只有一个,它是右操作数.而另一个左操作数则是(user-defined)对象本身,通过this传入.
      • 有点尴尬的是,如果左操作数不是类,就无法进行重载了.(因此需要non-member function进行补充).
        operator-overloading-member-function-vs-non-member-function
    • 另外我测试了一下,貌似如果non-member function与member function同时存在的话(调用原型相同),会调用member function.(g++ 4.8)不知是优先级较高还是未定义行为?有人知道的话麻烦留个言,多谢多谢~

Phase_1 the friend function way(9.2 quiz)

交作业:

  1 #include                                                                                                                                                                                                                                               
  2 class Fraction{                                                                 
  3     private:                                                                    
  4         int nmrt_;                                                              
  5         int dnmnt_;                                                             
  6     public:                                                                     
  7        /*                                                                       
  8         Fraction(int a, int b) : nmrt_(a), dnmnt_(b){                           
  9             if(!(nmrt_ % dnmnt_)){                                              
 10                 nmrt_ = nmrt_ / dnmnt_;                                         
 11                 dnmnt_ = 1;                                                     
 12             }else if(!(dnmnt_ % nmrt_)){                                        
 13                 dnmnt_ = dnmnt_ / nmrt_;                                        
 14                 nmrt_ = 1;                                                      
 15             }                                                                   
 16         }                                                                       
 17         */                                                                      
 18         Fraction(int a, int b) : nmrt_(a / gcd(a,b)), dnmnt_(b / gcd(a, b)){ }  
 19         void print();                                                           
 20         friend Fraction operator*(int a,const Fraction &m);                     
 21         friend Fraction operator*(const Fraction &m, int a);                    
 22         friend Fraction operator*(const Fraction &lhs, const Fraction &rhs);    
 23         static int gcd(int a, int b);                                           
 24 };                                                                              
 25                                                                                 
 26 inline void Fraction::print(){                                                  
 27     std::cout << nmrt_ << "/" << dnmnt_ <

Phase_2 关于使用引用参数/引用返回值

  • 先考虑non-member function的operator overloading.
    • 首先考虑一下引用返回值,另一篇《引用与指针》里有稍微提到,以引用作为返回值其实很蛋疼:
      • 你不能返回一个局部变量的引用.
      • 也不建议你new一个变量然后以它的指针返回.(因为这样需要在外面delete,这种写法容易造成内存泄漏,虽然这样不好,但也经常使用).
      • 更不建议new一个变量然后返回它的指针的解引用.(这样不仅容易内存泄漏更有看起来很恶心的语法).
      • 所以可能用来作为引用返回的只有可能是该函数的参数或者在类中的*this

  • 关于以值返回或以引用返回的一个general rule - 非常精辟:
    • an operator whose result is a new value (such as +, -, etc) must return the new value by value.(语义上来讲,a + b的结果不应该改变a或b任意一个人的值,那么operator重载如果要返回引用就无从返回了,不可能new一个东西,再返回它的引用,没有这样的写法)
    • an operator whose result is an existing value, but modified (such as <<, >>, +=, -=, [],etc), should return a reference to the modified value.
      • 必须以引用返回的例子:
      • operator << or operator >>:输入输出的重载一般类似friend std::ostream & operator<<(std::ostream & out, const myclass & y);
        • 流对象不可复制或赋值(private).因此只能返回引用.
        • 返回值能否是其他类型呢?一般不行,因为流操作符通常需要支持链式, 如 std::cout<< A << B << C < 结合顺序是 (((std::cout<< A) << B) << C )<
        • 所以流操作符重载只能老老实实返回流参数对象的引用
      • operator []:语义上来讲,你获取一个元素下标是有可能修改它的值的,如果返回了一个值,那你试图修改的只是一个临时对象,你也无法修改临时对象.
        • 只能是成员函数.
      • operator =:同样是为了支持链式,不过这个是从右往左的结合性.返回值也能运行正常,但有额外的复制拷贝的操作.所以是返回引用:return *this
        • 只能是成员函数.
        • 编译器默认生成一个基于浅拷贝copy-assignment operator=.
        • 新标准貌似引入了一些比如copy-swap- / move- assignment...我表示懵逼.

Phase 3_上代码 关于返回引用

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #define MAX 128
  6 class mystr{
  7     private:
  8         std::string data_;
  9     public:
 10         mystr() : data_(""){ }// once you define a constructor, compiler won't generate this default one.
 11         mystr(const std::string & str) : data_(str){ }//const-ref as para is ok & good practice.
 12 explicit mystr(const char* cstr) : data_(cstr){ }
 13         mystr(int x){
 14             char buf[MAX];
 15             snprintf(buf, MAX, "%d", x);
 16             data_ = std::string(buf);
 17         }
 18         mystr(const mystr& m) : data_(m.data_){}//this's like a default copy constructor
 19
 20         mystr& operator=(const mystr& m){// once you define a constructor, compiler won't generate this default     one.
 21             if(&m == this){
 22                 return *this;
 23             }
 24             data_ = m.data_;
 25             return *this;
 26         }
 27
 28         mystr& operator=(const char * cstr){
 29             data_ = cstr;
 30             return *this;
 31         }
 32
 33         void print(){
 34             printf("%s\n", data_.c_str());
 35         }
 36         mystr operator+(const mystr &x)//member operator overloading
 37         {
 38             printf("member func is called.\n");
 39             return mystr(this->data_ + x.data_);
 40         }
 41         char& operator[](int x)
 42         {
 43          //@return's type must be a ref. for modifying it.
 44          //if return as value, it is a temp-object which is not allowed to modify.
 45             assert(x < data_.size());
 46             return data_[x];
 47         }
 48
 49
 50         friend std::ostream & operator<<(std::ostream& out, const mystr &y);
 51         //@out is non-const because we will modified it.so as the @return.
 52         //@out & @return must be ref because stream object can't be copyed or assigned.
 53
 54         friend mystr operator+(const mystr &x, const mystr &y);//const is needed for auto-type-convert
 55 };
 56 //version 1
 57 mystr operator+(const mystr &x, const mystr &y)
 58 //here must be a const-ref para, or it won't auto-convert from const char*/int/std::string into mystr.
 59 {
 60     std::string s = x.data_;
 61     s.append(y.data_);
 62     return mystr(s);
 63 }
 64
 65 std::ostream& operator<<(std::ostream &out, const mystr &y)
 66 {
 67     return out << y.data_;
 68 }
 69
 70 //version 2
 71 //mystr operator+(mystr &x, mystr &y)
 72 //{
 73 //    return mystr(x.data_ + y.data_);
 74 //}
 75
 76 int test1()//const-ref as para is ok & good practice.
 77 {
 78     std::string * ss = new std::string("12345");
 79     mystr a(*ss);
 80     delete ss;
 81     a.print();
 82     return 0;
 83 }
 84
 85 int test2()//
 86 {
 87     mystr ss = mystr("123") + mystr("abc");
 88     ss.print();
 89     return 0;
 90 }
 91
 92 int test3()
 93 {
 94     std::cout << mystr("1234") << mystr("2345") << std::endl;
 95     return 0;
 96 }
 97
 98 int test4()//test for operator[] & modify it.
 99 {
100     mystr ss("01234");
101
102     std::cout << ss[0] << std::endl;
103
104     ss[0] = 'A';
105
106     std::cout << ss << std::endl;
107
108     return 0;
109 }
110
111 int test5()
112 {
113     mystr ss("1234");
114     mystr s2;
115     s2 = ss;
116     std::cout << s2 << std::endl;
117     return 0;
118 }
119
120 int main(){
121     return test5();
122 }

下面摘录了一些资料

The Three Basic Rules of Operator Overloading in C++
When it comes to operator overloading in C++, there are three basic rules you should follow. As with all such rules, there are indeed exceptions. Sometimes people have deviated from them and the outcome was not bad code, but such positive deviations are few and far between. At the very least, 99 out of 100 such deviations I have seen were unjustified. However, it might just as well have been 999 out of 1000. So you’d better stick to the following rules.
Whenever the meaning of an operator is not obviously clear and undisputed, it should not be overloaded. Instead, provide a function with a well-chosen name.Basically, the first and foremost rule for overloading operators, at its very heart, says: Don’t do it. That might seem strange, because there is a lot to be known about operator overloading and so a lot of articles, book chapters, and other texts deal with all this. But despite this seemingly obvious evidence, there are only a surprisingly few cases where operator overloading is appropriate. The reason is that actually it is hard to understand the semantics behind the application of an operator unless the use of the operator in the application domain is well known and undisputed. Contrary to popular belief, this is hardly ever the case.

Always stick to the operator’s well-known semantics.
C++ poses no limitations on the semantics of overloaded operators. Your compiler will happily accept code that implements the binary + operator to subtract from its right operand. However, the users of such an operator would never suspect the expression a + b to subtract a from b. Of course, this supposes that the semantics of the operator in the application domain is undisputed.

Always provide all out of a set of related operations.
Operators are related to each other* and to other operations.
If your type supportsa + b, users will expect to be able to call a += b
, too.
If it supports prefix increment ++a, they will expect a++ to work as well.
If they can check whether a < b, they will most certainly expect to also to be able to check whether a > b.
If they can copy-construct your type, they expect assignment to work as well.


C++98/03 what the compiler generate for us ?##

If they are needed,
the compiler will generate a default constructor for you unless you declare any constructor of your own.
the compiler will generate a copy constructor for you unless you declare your own.
the compiler will generate a copy assignment operator for you unless you declare your own.
the compiler will generate a destructor for you unless you declare your own.


When to use a normal, friend, or member function overload

In most cases, the language leaves it up to you to determine whether you want to use the normal/friend or member function version of the overload. However, one of the two is usually a better choice than the other.

  • When dealing with binary operators that don’t modify the left operand (e.g. operator+), the normal or friend function version is typically preferred, because it works for all parameter types (even when the left operand isn’t a class object, or is a class that is not modifiable). The normal or friend function version has the added benefit of “symmetry(对称型)”, as all operands become explicit parameters (instead of the left operand becoming this and the right operand becoming an explicit parameter).
  • When dealing with binary operators that do modify the left operand (e.g. operator+=), the member function version is typically preferred. In these cases, the leftmost operand will always be a class type, and having the object being modified become the one pointed to by *this is natural. Because the rightmost operand becomes an explicit parameter, there’s no confusion over who is getting modified and who is getting evaluated.
  • Unary operators(一元操作符) are usually overloaded as member functions as well, since the member version has no parameters.

The following rules of thumb can help you determine which form is best for a given situation:

  • If you’re overloading assignment (=), subscript ([]), function call (()), or member selection (->), do so as a member function.(只能如此,见ref的截图)
  • If you’re overloading a unary operator, do so as a member function.
  • If you’re overloading a binary operator that modifies its left operand (e.g. operator+=), do so as a member function if you can.
  • If you’re overloading a binary operator that does not modify its left operand (e.g. operator+), do so as a normal function or friend function.

你可能感兴趣的:([cpp deep dive] `operator` deep dive)