确保您定义的重载操作符不被弃置
目录
操作符在c/c++中本质就是函数,是具有特殊的函数名的函数。那么作为函数,标准库内置实现这些操作符当然就顺理成章采用函数模板的方式来实现。因此,在篇一时为类类型做操作符重载,都是针对类类型本身,不采用模板方式,就是为了避免与内置操作符提供能的内置功能起冲突,使得编译器无法在调用重载操作符时进行重载决议。
一、 内置赋值操作符-函数模板
赋值运算符是编写代码中最常用的操作符运用之一,可以用于修改对象的值。:
int a = 10
int b = a;
a += 1;
b +=a;
int c = a + b;
c *= a;
c = a * b;
int d =c%a;
d = c/2;
int e = d<<1;
e >>=d;
c/c++标准库是通过定义一些列的复杂赋值操作符函数模板来为开发者提供支持的:
运算符名 语法 可重载 类内定义 类外定义
//T2 可为包含 T 在内的任何类型
简单赋值 a = b 是 T& T::operator =(const T2& b); N/A
加法赋值 a += b 是 T& T::operator +=(const T2& b); T& operator +=(T& a, const T2& b);
减法赋值 a -= b 是 T& T::operator -=(const T2& b); T& operator -=(T& a, const T2& b);
乘法赋值 a *= b 是 T& T::operator *=(const T2& b); T& operator *=(T& a, const T2& b);
除法赋值 a /= b 是 T& T::operator /=(const T2& b); T& operator /=(T& a, const T2& b);
取模赋值 a %= b 是 T& T::operator %=(const T2& b); T& operator %=(T& a, const T2& b);
逐位与赋值 a &= b 是 T& T::operator &=(const T2& b); T& operator &=(T& a, const T2& b);
逐位或赋值 a |= b 是 T& T::operator |=(const T2& b); T& operator |=(T& a, const T2& b);
逐位异或赋值 a ^= b 是 T& T::operator ^=(const T2& b); T& operator ^=(T& a, const T2& b);
逐位左移赋值 a <<= b 是 T& T::operator <<=(const T2& b); T& operator <<=(T& a, const T2& b);
逐位右移赋值 a >>= b 是 T& T::operator >>=(const T2& b); T& operator >>=(T& a, const T2& b);
所有内建赋值运算符都返回 *this,而大多数用户定义重载亦返回 *this,从而能以与内建版本相同的方式使用用户定义运算符。在用户定义运算符重载中,能以任何类型为返回类型(包括 void)。
二、直接赋值
直接赋值运算符期待以一个可修改左值为其左操作数,以一个右值表达式或花括号初始化器列表 (C++11 起)为其右操作数,并返回一个标识修改后的左操作数的左值。直接赋值是针对非类类型对象的,其表达式形式为:
左操作数 = 右操作数
左操作数 = {} //(C++11 起)
左操作数 = { 右操作数 } //(C++11 起)
对于内建赋值运算,左操作数可拥有任何非 const 标量类型,而右操作数必须可隐式转换为左操作数的类型。对于非类类型,首先将右操作数隐式转换为左操作数的无 cv 限定的类型,然后复制其值到左操作数所标识的对象中。当左操作数拥有引用类型时,赋值运算符修改被引用的对象。若左右操作数标识的对象之间有重叠,则行为未定义(除非二者严格重叠且类型相同)。
对于非类类型的赋值应用:
int a = 0; //不是赋值
a = 1; //直接赋值
int b = {}; //零初始化,然后赋值
b = 'a'; //整型提升,然后赋值
int c = int{}; //零初始化,然后构造赋值
c = 'b'; //显式转型,然后赋值
int d = {0}; //显式零初始化,然后赋值
d = 1.0; //浮点转换,然后赋值
int e = int{0};//显式零初始化,然后构造赋值
int f = int(); //零初始化,然后构造赋值
int g = int(0);//显式零初始化,然后构造赋值
对于类类型的赋值运用,若表达式 T拥有类类型,则语法 T = {args...} 生成以花括号初始化器列表为实参对赋值运算符的一次调用,然后遵循重载决议规则选取适合的赋值运算符。
//test1.h
class Obj
{
public:
Obj(int val_=0);
~Obj();
private:
int val;
};
//test1.cpp
Obj::Obj(int val_) : val(val_)
{
};
Obj::~Obj()
{
};
//main.cpp
Obj oa = {0};
Obj ob = Obj{0};
Obj oc = {};
Obj od = Obj{};
需要注意的是,若以某个非类类型为实参的非模板赋值运算符可用,则它胜过 T = {} 中的复制/移动赋值,这是因为从 {} 到非类类型属于恒等转换,它优先于从 {} 到类类型的用户定义转换。
三、 复制赋值操作符
3.1 复制赋值运算操作符语法
复制赋值运算符以 b 内容的副本替换对象 a 的内容(不修改 b)。对于类类型,这是一种特殊成员函数,描述于复制赋值运算符。复制赋值运算符语法形式如下:
类名 & 类名 :: operator= ( 类名 ) //复制赋值运算符在采用复制交换法时的典型声明。
类名 & 类名 :: operator= ( const 类名 & ) //复制赋值运算符在不采用复制交换法时的典型声明。
类名 & 类名 :: operator= ( const 类名 & ) = default;//(C++11 起),强制编译器生成复制赋值运算符。
类名 & 类名 :: operator= ( const 类名 & ) = delete; //(C++11 起),避免隐式复制赋值。
类 T 的复制赋值运算符是名为 operator= 的非模板非静态成员函数,它接受恰好一个 T、T&、const T&、volatile T& 或 const volatile T& 类型的形参。可复制赋值 (CopyAssignable) 类型必须有公开的复制赋值运算符。编译器会根据表达式语句,在重载决议选择复制赋值运算符时,它们会被决议规则确定而调用。
3.2 复制赋值操作符隐式声明
对于类类型(struct、class 或 union)来说,如果没有提供任何用户定义的复制赋值运算符,那么编译器将始终声明一个,作为类的 inline public 成员。如果满足下列所有条件,
1)T 的每个直接基类 B 均拥有形参为 B,const B& 或 const volatile B& 的复制赋值运算符;
2)T 的每个类类型或类数组类型的非静态数据成员 M 均拥有形参为 M,const M& 或 const volatile M& 的复制赋值运算符。
那么这个隐式声明的复制赋值运算符拥有形式:
T& T::operator=(const T&)
类可以拥有多个复制赋值运算符,如 T& T::operator=(T&) 和 T& T::operator=(T)。当存在用户定义的复制赋值运算符时,C++11 起,用户可以通过关键词 default 强迫编译器生成隐式声明的复制赋值运算符。
class Obj
{
public:
//...other
// Obj& operator=(const Obj& rhs); //一元操作符,赋值运算=
Obj& operator=(const Obj& rhs) = default;//隐式赋值运算=
private:
int val;
};
//
Obj a = {10};
Obj b = a;
因为每个类总是会声明复制赋值运算符,所以基类的赋值运算符始终会被隐藏。当使用 using 声明从基类带入复制赋值运算符,且它的实参类型与派生类的隐式复制赋值运算符的实参类型相同时,该 using 声明也会被隐式声明隐藏。
3.3 隐式声明复制赋值操作符
如果满足下列任一条件,那么类 T
的隐式声明的复制赋值运算符被定义为弃置的,否则它就被定义为预置的赋值运算符:
T
拥有用户声明的移动构造函数;T
拥有用户声明的移动赋值运算符。
类 T 的移动构造函数是非模板构造函数,它的首个形参是 T&&、const T&&、volatile T&& 或 const volatile T&&,且没有其他形参,或剩余形参均有默认值。
//移动构造语法。类名 必须指名当前类(或类模板的当前实例化),或在命名空间作用域或友元声明中声明时,必须是有限定的类名,C++11 起
类名 ( 类名 && ); //移动构造函数的典型声明。
类名 ( 类名 && ) = default; //强制编译器生成移动构造函数。
类名 ( 类名 && ) = delete; //避免隐式生成移动构造函数。
移动赋值函数的特点见下一节,这里先不展开。
当需要为类型 T
的复制赋值运算符做隐式声明时,就不能用户声明移动构造函数和移动赋值运算符。
class Obj
{
public:
//...other
// Obj& Obj(const Obj&& rhs); //移动构造开启,隐式赋值无效
// Obj& operator=(const Obj& rhs); //一元操作符,赋值运算=
//Obj& operator=(const Obj&& rhs); //移动赋值开启,隐式赋值无效
Obj& operator=(const Obj& rhs) = default;//隐式赋值运算=
private:
int val;
};
满足上述两个条件,就会被定义为预置的复制赋值运算符,如果满足下列任一条件,那么类 T
的预置的复制赋值运算符还会被定义为弃置的:
- T 拥有具有 const 限定的非类类型(或其数组)的非静态数据成员;
- T 拥有引用类型的非静态数据成员;
- T 拥有无法复制赋值的非静态数据成员,直接基类或虚基类(对复制赋值的重载决议失败,或选择弃置或不可访问的函数);
- T 是联合体式的类,且拥有的某个变体成员对应的复制赋值运算符是非平凡的
class Test{};
class Base{ public: Test *ptr; };
class Obj : public Base 隐式赋值运算=被弃置
{
public:
//...other
Obj& operator=(const Obj& rhs) = default;//隐式赋值运算=
private:
int val;
Test *ptr; //隐式赋值运算=被弃置
const int a = 10; //隐式赋值运算=被弃置
};
3.4 主动禁止复制赋值操作符
有些类需要完全禁止复制。例如,iostream 类就不允许复制。如果想要禁止复制,似乎可以省略复制构造函数,然而,如果不定义复制构造函数,编译器将合成一个。为了防止复制,类必须显式声明其复制构造函数为 private。
//test1.h
class Obj
{
public:
//...other
private:
// Obj& operator=(const Obj& rhs){return *this;}; //一元操作符,赋值运算=,不做任何处理
Obj& operator=(const Obj&);
// Obj& operator=(const Obj&) = default;
// Obj& operator=(Obj&) = default;
private:
int val;
};
//test1.cpp
Obj& Obj::operator=(const Obj& rhs)
{
if(this==&rhs)
return *this;
val = rhs.val;
return *this;
};
//main.cpp
Obj a = 100;
Obj b = 10;
b = a; //error,赋值函数为私有,不能赋值
Obj c = a; //OK,调用的不是赋值函数,而是隐式拷贝构造
如果复制构造函数是私有的,将不允许用户代码复制该类类型的对象,编译器将拒绝任何进行复制的尝试。然而,类的友元和成员仍可以进行复制。如果想要连友元和成员中的复制也禁止,就可以声明一个(private)复制构造函数但不对其定义,或者更彻底禁止它。
//test1.h
class Obj
{
public:
//...other
friend class ATest;
private:
// Obj& operator=(const Obj& rhs){return *this;}; //一元操作符,赋值运算=,不做任何处理
Obj& operator=(const Obj&);
// Obj& operator=(const Obj&) = default;
// Obj& operator=(Obj&) = default;
private:
int val;
};
//test1.cpp
Obj& Obj::operator=(const Obj& rhs)
{
if(this==&rhs)
return *this;
val = rhs.val;
return *this;
};
//main.cpp
class ATest
{
public:
void test()
{
Obj o_org = 100;
Obj o_cpy = 10;
std::cout << "o_org = " << o_org << "\n";
o_cpy = o_org; //OK,友元可以直接使用
std::cout << "o_cpy = " << o_cpy << "\n";
};
};
//
ATest at;
at.test();
如前面复制赋值运算符语法形式展示的那样,类类型还可以通过关键词delete强迫复制赋值运算符无效,禁止复制赋值运算,该声明可以放在public、protected、private都可以达到相同效果。
class Obj
{
public:
//...other
friend class ATest;
private:
// Obj& operator=(const Obj& rhs){return *this;}; //一元操作符,赋值运算=,不做任何处理
// Obj& operator=(const Obj&);
// Obj& operator=(const Obj&) = default;
// Obj& operator=(Obj&) = default;
public:
Obj& operator=(const Obj&) = delete;
Obj& operator=(Obj&) = delete;
private:
int val;
};
//main.cpp
class ATest
{
public:
void test()
{
Obj o_org = 100;
Obj o_cpy = 10;
std::cout << "o_org = " << o_org << "\n";
o_cpy = o_org; //error,无法编译,赋值函数被强制删除,不能赋值
std::cout << "o_cpy = " << o_cpy << "\n";
};
};
//main.cpp main函数体
Obj a = 100;
Obj b = 10;
b = a; //error,无法编译,赋值函数被强制删除,不能赋值
//
ATest at;
at.test();
3.5 平凡的复制赋值运算符
如果满足下列所有条件,那么类 T 的复制赋值运算符是平凡的:
- 它不是用户提供的(即它是隐式定义或预置的);
- T 没有虚成员函数;
- T 没有虚基类;
- 为 T 的每个直接基类选择的复制赋值运算符都是平凡的;
- 为 T 的每个类类型(或类类型的数组)的非静态数据成员选择的复制赋值运算符都是平凡的。
平凡复制赋值运算符如同用 std::memmove 进行对象表示的复制。所有与 C 语言兼容的数据类型(POD 类型)都可以平凡复制。
struct DataS
{
int ival;
char cval;
float fval;
};
class DataC
{
private:
/* data */
int ival;
char cval;
float fval;
public:
DataC(/* args */);
~DataC();
};
DataC::DataC(/* args */){ }
DataC::~DataC(){ }
//
DataS d1,d2;
d2 = d1; //OK
DataC c1, c2;
c2 = c1; //OK
3.6 合格的复制赋值运算符要求
合格复制赋值运算符的平凡性确定该类是否为可平凡复制类型:
*被用户声明或者同时被隐式声明且可定义的复制赋值运算符是合格的。 (C++11 前)
*没有被弃置的复制赋值运算符是合格的。 (C++11 起)(C++20 前)
*满足下列所有条件的复制赋值运算符是合格的:
1)它没有被弃置,且
2)满足它的所有关联约束(如果存在),且
3)没有比它更受约束且拥有相同的第一形参类型和相同的 cv 或引用限定符(如果存在)的复制赋值运算符。(C++20 起)
如果隐式声明的复制赋值运算符既没有被弃置也不平凡,那么当它被 ODR 式使用或用于常量求值 (C++14 起)时,它会被编译器定义(即生成并编译函数体)。对于联合体类型,隐式定义的复制赋值运算符(如同以 std::memmove)复制其对象表示。对于非联合类类型(class 与 struct),编译器按照声明顺序对对象的各基类和非静态成员进行逐成员复制赋值,其中对标量进行内建赋值,而对类类型使用复制赋值运算符,如果满足下列所有条件,那么类 T 的隐式定义的复制赋值运算符是 constexpr 的:
- T 是字面类型,且
- 复制每个直接基类子对象时选中的赋值运算符都是 constexpr 函数,且
- 复制 T 的每个类(或其数组)类型的数据成员时选中的赋值运算符都是 constexpr 函数。 (C++14 起)
当 T 类型拥有用户定义的析构函数或用户定义的复制赋值运算符时,隐式定义的复制赋值运算符的生成被弃用。 (C++11 起)
四、 移动赋值操作符
4.1 移动赋值操作符语法
移动赋值运算符以 b 的内容替换对象 a 的内容,并尽可能避免复制(可以修改 b)。对于类类型,这是一种特殊成员函数,描述于移动赋值运算符。 移动赋值运算符是C++11 起开始支持的,其语法形式为:
//(C++11 起)
类名 & 类名 :: operator= ( 类名 && ); //移动赋值运算符的典型声明。
类名 & 类名 :: operator= ( 类名 && ) = default;//强制编译器生成移动赋值运算符。
类名 & 类名 :: operator= ( 类名 && ) = delete; //禁止隐式移动赋值。
类类型 T 的移动赋值运算符是名为 operator=的非模板非静态成员函数,它接受恰好一个 T&&、const T&&、volatile T&& 或 const volatile T&& 类型的形参。
每当重载决议选择移动赋值运算符时,它都会被调用,例如当对象出现在赋值表达式左侧,而其右侧是同类型或可隐式转换的类型的右值时。典型的移动赋值运算符“窃取”实参曾保有的资源(例如指向动态分配对象的指针,文件描述符,TCP socket,输入输出流,运行的线程,等等),而非复制它们,并使得实参遗留在某个合法但不确定的状态。例如,从 std::string 或从 std::vector 移动赋值可能导致实参被置空。然而这并不保证会发生。移动赋值与普通赋值相比,其定义较为宽松而非更严格;在完成时,普通赋值必须留下数据的两份副本,而移动赋值只要求留下一份。
//test2.h
#ifndef _TEST_2_H_
#define _TEST_2_H_
class ObjF
{
public:
ObjF& operator=(const ObjF& rhs); //
ObjF& operator=(const ObjF&& rhs); //
private:
float val;
};
#endif //_TEST_2_H_
//test2.cpp
#include "test2.h"
#include <iostream>
#include <utility>
ObjF& ObjF::operator=(const ObjF& rhs) //复制赋值
{
if(this==&rhs)
return *this;
val = rhs.val;
std::cout << "copy operator= call\n";
return *this;
};
ObjF& ObjF::operator=(const ObjF&& rhs)//移动赋值
{
val = std::move(rhs.val);
std::cout << "move operator= call\n";
return *this;
};
//main.cpp
//
ObjF of1,of2,of3;
of2 = of1; //copy operator= call
of3 = std::move(of1); //move operator= call
4.2 隐式声明的移动赋值运算符
如果没有对类类型(struct、class 或 union)提供任何用户定义的移动赋值运算符,且满足下列所有条件:
- 没有用户声明的复制构造函数;
- 没有用户声明的移动构造函数;
- 没有用户声明的复制赋值运算符;
- 没有用户声明的析构函数,
那么编译器将声明一个默认移动赋值运算符,作为类的 inline public 成员,并拥有签名 T& T::operator=(T&&)。类可以拥有多个移动赋值运算符,如 T& T::operator=(const T&&) 和 T& T::operator=(T&&)。
#ifndef _TEST_2_H_
#define _TEST_2_H_
class ObjF
{
public:
ObjF& operator=(const ObjF& rhs); //
ObjF& operator=(const ObjF&& ); //
ObjF& operator=(ObjF&& ); //
ObjF& operator=(const float&& ); //
ObjF& operator=(float&& ); //
private:
float val;
};
#endif //_TEST_2_H_
当存在用户定义的移动赋值运算符时,用户仍然可以通过关键词 default 强迫编译器生成隐式声明的移动赋值运算符。
#ifndef _TEST_2_H_
#define _TEST_2_H_
class ObjF
{
public:
ObjF& operator=(const ObjF& rhs); //
ObjF& operator=(const ObjF&& ); //
ObjF& operator=(ObjF&& ) noexcept = default; //隐式移动赋值,支持noexcept动态异常说明符
// ObjF& operator=(ObjF&& ); //
ObjF& operator=(const float&& ); //
ObjF& operator=(float&& ); //
private:
float val;
};
#endif //_TEST_2_H_
隐式声明(或在其首个声明被预置)的移动赋值运算符具有动态异常说明 (C++17前)noexcept 说明 (C++17 起)中所描述的异常说明。
因为每个类总是会声明赋值运算符(移动或复制),所以基类的赋值运算符始终被隐藏。当使用 using 声明从基类带入赋值运算符,且它的实参类型与派生类的隐式赋值运算符的实参类型相同时,该 using 声明也会被隐式声明隐藏。
4.3 弃置的移动赋值操作符
类似于复制复制运算操作符,如果满足下列任一条件,那么类 T 的隐式声明或预置的移动赋值运算符被定义为弃置的:
- T 拥有 const 限定的非静态数据成员。
- T 拥有引用类型的非静态数据成员。
- T 拥有无法移动赋值(拥有被弃置、不可访问或有歧义的移动赋值运算符)的非静态数据成员。
- T 拥有无法移动赋值(拥有被弃置、不可访问或有歧义的移动赋值运算符)的直接基类或虚基类。
满足以上任一条件时,重载决议原则会忽略被弃置的隐式声明的移动赋值运算符。
class Test{};
class Base{ public: Test *ptr; };
class Obj : public Base //隐式移动赋值运算=被弃置
{
public:
//...other
Obj& operator=(const Obj&& rhs) noexcept = default;//移动赋值运算=
private:
int val;
Test *ptr; //隐式移动赋值运算=被弃置
const int a = 10; //隐式移动赋值运算=被弃置
};
4.4 平凡的移动赋值运算符
如果满足下列所有条件,那么类 T 的移动赋值运算符是平凡的:
- 它不是用户提供的(即它是隐式定义或预置的);
- T 没有虚成员函数;
- T 没有虚基类;
- 为 T 的每个直接基类选择的移动赋值运算符都是平凡的;
- 为 T 的每个类类型(或类类型的数组)的非静态数据成员选择的移动赋值运算符都是平凡的;
平凡移动赋值运算符实施与平凡复制赋值运算符相同的动作,即如同以 std::memmove 进行对象表示的复制。所有与 C 兼容的数据类型(POD 类型)都可以平凡移动。
struct DataS
{
int ival;
char cval;
float fval;
};
class DataC
{
private:
/* data */
int ival;
char cval;
float fval;
public:
DataC(/* args */);
~DataC();
};
DataC::DataC(/* args */){}
DataC::~DataC(){}
//main.cpp main函数体内
DataS d3,d4;
d4 = std::move(d3);
DataC c3, c4;
c4 = std::move(c3);
4.5 合格的移动赋值操作符
合格的移动赋值运算符要求:
*没有被弃置的移动赋值运算符是合格的。 (C++20 前)
*满足下列所有条件的移动赋值运算符是合格的:
1)它没有被弃置,且
2)满足它的所有关联约束(如果存在),且
3)没有比它更受约束且拥有相同的第一形参类型和相同的 cv 或引用限定符(如果存在)的移动赋值运算符。 (C++20 起)
合格移动赋值运算符的平凡性确定该类是否为该类是否为可平凡复制类型。
隐式定义的移动赋值运算符,如果隐式声明的移动赋值运算符既没有被弃置也不平凡,那么当它被 ODR 式使用或用于常量求值 (C++14 起)时,它会被编译器定义(即生成并编译函数体)。
对于联合体类型,隐式定义的移动赋值运算符(如用 std::memmove)复制其对象表示。
对于非联合类类型(class 与 struct),移动赋值运算符按照声明顺序对对象的各直接基类和直接非静态成员进行完整的逐成员移动赋值,其中对标量用内建运算符,对数组用逐元素移动赋值,而对类类型用移动赋值运算符(非虚调用)。
如果满足下列所有条件,那么类 T 的隐式定义的复制赋值运算符是 constexpr 的:
- T 是字面类型,且
- 移动每个直接基类子对象时选中的赋值运算符都是 constexpr 函数,且
- 移动 T 的每个类(或其数组)类型的数据成员时选中的赋值运算符都是 constexpr 函数。 (C++14 起)
struct DataS
{
int ival;
char cval;
float fval;
};
class DataC
{
private:
/* data */
int ival;
char cval;
float fval;
public:
DataC(/* args */);
~DataC();
};
DataC::DataC(/* args */)
{
}
DataC::~DataC()
{
}
class DataChild : public DataC //基类支持constexpr
{
public:
DataChild& operator=(DataChild&& ) noexcept = default; //OK
private:
int val; //支持constexpr
DataS ds; //成员变量支持constexpr
};
//
DataChild dc1,dc2;
dc2 = std::move(dc1);
与复制赋值一样,隐式定义的移动赋值运算符是否会多次对在继承网格中可通过多于一条路径访问的虚基类子对象赋值是未指明的:
struct V
{
V& operator=(V&& other) {
// 这可能会被调用一或两次
// 如果调用两次,那么 'other' 是刚被移动的 V 子对象
return *this;
}
};
struct A : virtual V { }; // operator= 调用 V::operator=
struct B : virtual V { }; // operator= 调用 V::operator=
struct C : B, A { }; // operator= 调用 B::operator=,然后调用 A::operator=
// 但可能只调用一次 V::operator=
//
C c1, c2;
c2 = std::move(c1);
五、复合赋值操作符
复合赋值(compound assignment)运算符以 a 的值和 b 的值间的二元运算结果替换对象 a 的内容。复合赋值表达式的形式为:
//左操作数,对于内建运算符,左操作数可具有任何算术类型,但若运算符为+= 或-=,则也接受指针类型,并与+和-有相同限制.
//运算符,*=、/=、%=、+=、-=、<<=、>>=、&=、^=、|= 之一
//右操作数, 对于内建运算符,右操作数必须可隐式转换为左操作数
左操作数 运算符 右操作数
左操作数 运算符 {} //(C++11 起)
左操作数 运算符 { 右操作数 } //(C++11 起)
每个内建复合赋值运算符表达式 E1 op= E2(其中 E1 是可修改左值表达式,而 E2 是右值表达式或花括号初始化器列表 (C++11 起))的行为与表达式 E1 = E1 op E2 的行为严格相同,但只对表达式 E1 进行一次求值,并且对于顺序不确定的函数而言是一次单个操作(例如 f(a += b, g()) 中,从 g() 内来看,+= 要么完全未开始,要么已完成)。
/*
A1& operator*=(A1&, A2);
A1& operator/=(A1&, A2);
A1& operator+=(A1&, A2);
A1& operator-=(A1&, A2);
*/
//依据参与重载决议规则,对每对 A1 和 A2,其中 A1 是算术类型,而 A2 为提升后的算术类型,
int a = 10;
a *=1.0;
a /='a';
a -= true;
类成员的合成赋值操作符,它会根据成员类型使用适合的内置或类定义的赋值操作符,执行逐个成员赋值:右操作数对象的每个成员赋值给左操作数对象的对应成员。除数组之外,每个成员用所属类型的常规方式进行赋值。对于数组,给每个数组元素赋值。合成赋值操作符返回 *this,它是对左操作数对象的引用。
复制和赋值常一起使用,一般的类类型无须定义赋值操作符,这些默认赋值操作符版本工作得很好。然而,类也可以定义自己的赋值操作符。一般而言,如果类需要复制构造函数,它也会需要赋值操作符。实际上,就将这两个操作符看作一个单元。如果需要其中一个,我们几乎也肯定需要另一个。
六、重载决议
6.1 重载操作符调用决议
重载决议,就是c/c++编译时,为了编译函数调用,编译器必须首先进行名字查找,对于函数可能涉及实参依赖查找,而对于函数模板可能后随模板实参推导。如果这些步骤产生了多个候选函数,那么需要进行重载决议选择将要实际调用的函数。
通常来说,所调用的函数是各形参与各实参之间的匹配最紧密的候选函数。如果函数无法被重载决议选择(例如它是有未被满足的约束的模板化实体),那么不能指名或再使用它。如果重载决议可以做成选择,那么将指向重载函数的地址。
如果复制和移动赋值运算符都有提供,那么重载决议会在实参是右值(例如无名临时量的纯右值 或 std::move 的结果的亡值)时选择移动赋值,而在实参是左值(具名对象或返回左值引用的函数或运算符)时选择复制赋值。
如果只提供了复制赋值,那么重载决议对于所有值类别都会选择它(只要它按值或按到 const 的引用接收其实参),从而当移动赋值不可用时,复制赋值将会成为它的后备。隐式定义的移动赋值运算符是否会多次对在继承网格中可通过多于一条路径访问的虚基类子对象赋值是未指明的(同样适用于复制赋值)。
6.2 重载决议与函数语境
函数调用运算符为任何对象提供函数语义,依据重载函数的名字及上下语境选择,除了函数调用表达式之外,可以出现在下列 7 种发生重载决议的语境中:
- 对象或引用声明中的初始化器
- 赋值运算符的右侧
- 作为函数调用的实参
- 作为用户定义运算符的实参
- return 语句
- 显式转型或 static_cast 的实参
- 非类型模板实参
在每个语境中,重载函数的名字可以前附取址运算符 & 并且可以被一组冗余的括号所环绕。在所有这些语境中,从重载集中选择的函数,是其类型与目标所期待的函数指针、函数引用或成员函数指针类型相匹配的函数,目标分别为:被初始化的对象或引用,赋值的左侧,函数或运算符的形参,函数的返回类型,转型的目标类型,以及模板形参的类型。
函数的形参类型和返回类型必须与目标严格匹配,不考虑隐式转换(例如,在初始化指向返回基类指针的函数的指针时,不会选择返回派生类指针的函数)。
如果函数名指名了某个函数模板,那么首先进行模板实参推导,且如果它成功,那么将会生成一个单独的模板特化并添加到所要考虑的重载集合中。从集合中丢弃所有不满足其关联约束的函数。
//test3.h
#ifndef _TEST_3_H_
#define _TEST_3_H_
void funcAddr_test();
#endif //
//test3.cpp
#include "test3.h"
int f(int) { return 1; }
int f(double) { return 2; }
void g( int(&f1)(int), int(*f2)(double) ) {}
template< int(*F)(int) >
struct Templ {};
struct Foo {
int mf(int) { return 3; }
int mf(double) { return 4; }
};
struct Emp {
void operator<<(int (*)(double)) {}
};
void funcAddr_test()
{
// 1. 初始化
int (*pf)(double) = f; // 选择 int f(double)
int (&rf)(int) = f; // 选择 int f(int)
int (Foo::*mpf)(int) = &Foo::mf; // 选择 int mf(int)
// 2. 赋值
pf = nullptr;
pf = &f; // 选择 int f(double)
// 3. 函数实参
g(f, f); // 为第一实参选择 int f(int)
// 为第二实参选择 int f(double)
// 4. 用户定义运算符
Emp{} << f; // 选择 int f(double)
// 5. 返回值
auto foo = []() -> int (*)(int) {
return f; // 选择 int f(int)
};
// 6. 转型
auto p = static_cast<int(*)(int)>(f); // 选择 int f(int)
// 7. 模板实参
Templ<f> t; // 选择 int f(int)
}
//main.cpp
funcAddr_test();
C++20 起,如果集合中有多于一个函数与目标匹配,且至少一个函数是非模板,那么从考虑集合中去除模板特化。对于任何一对非模板函数,如果其中一个比另一个更受约束,那么从集合中丢弃受较少约束的函数。 如果所有剩余候选者都是模板特化,那么当存在更特殊的模板特化时,移除较不特殊者。如果在各项移除之后还有多于一个候选者,那么程序非良构。
6.3 候选函数集
重载决议(函数调用)开始前,将名字查找和模板实参推导所选择的函数组成候选函数的集合(确切的判别标准取决于发生重载决议的语境)。
如果有候选函数是除构造函数外的成员函数(静态或非静态),那么将它当做如同它有一个额外形参(隐式对象形参),代表调用函数所用的对象,并出现在首个实际形参之前。类似地,调用成员函数所用的对象会作为隐含对象实参前附于实参列表。
对于类类型T 的成员函数,隐含对象形参的类型受成员函数的 cv 限定和引用限定影响,如成员函数中所述。就确定隐式对象形参类型而言,用户定义转换函数被认为是隐含对象实参的成员。以及由 using 声明引入到派生类中的成员函数被认为是派生类的成员。
对于静态成员函数,其隐式对象形参被认为匹配任何对象:不检验其类型,且不为其尝试转换序列。
对于重载决议的剩余部分,隐含对象实参与其他实参不可辨别,但下列特殊规则适用于隐式对象形参:
- 不能对隐式对象形参运用用户定义转换
- 右值能绑定到非 const 的隐式对象形参(除非是对引用限定的成员函数) (C++11 起),且不影响隐式转换的等级。
struct B { void f(int); };
struct A { operator B&(); };
A a;
a.B::f(1); // 错误:不能对隐式对象形参运用用户定义转换
static_cast<B&>(a).f(1); // OK
如果有候选函数是函数模板,那么使用模板实参推导生成其特化,并把这种特化当做非模板函数对待,但在决断规则中另行有所规定。如果一个名字指代一个或多个函数模板,并且同时指代重载的非模板函数,那么这些函数和从模板生成的特化都是候选。
template <typename T>
void doit(const T& obj)
{
//code
};
//调用
doit<int>(10);
//本质上编译器生成,给与调用
template <>
void doit<int>(const T& obj)
{
//code
};
如果构造函数模板或转换函数模板拥有在推导后它恰好为值待决的条件性 explicit 说明符,且语境要求非 explicit 的候选而所生成的候选为 explicit,那么从候选集中移除它。 (C++20 起)
候选函数列表中始终不包含被定义为弃置的预置移动构造函数和移动赋值运算符。在构造派生类对象时,候选函数列表中不包含继承的复制和移动构造函数。
6.4 候选函数准备及排序
使用重载决议的每种语境都以其独有的方式准备其候选函数集合和实参列表:
【1】调用具名函数
如果 E 在函数调用表达式 E(args) 中指名重载的函数和/或函数模板(但非可调用对象)的集合,那么遵循下列规则:
- 如果表达式 E 具有 pA->B 或 A.B 的形式(其中 A 具有类类型 cv T),那么将 B 作为 T 的成员函数查找。该查找所找到的函数声明都是候选函数。就重载决议而言,实参列表拥有 cv T 类型的隐含对象实参。
- 如果表达式 E 是初等表达式,那么遵循函数调用的正常规则查找它的名字(可能涉及 ADL)。该查找所找到的函数声明(取决于查找的工作方式)为下列之一:
- 全部是非成员函数(该情况下,就重载决议而言,实参列表正是函数调用表达式中所用的实参列表)
- 全部是某个类 T 的成员函数,该情况下,如果 this 在作用域中且为指向 T 或从 T 派生的类的指针,则以 *this 为隐含对象实参。否则(如果 this 不在作用域中或不指向 T),以一个 T 类型的虚假对象为隐含对象实参,而如果重载决议继而选择了非静态成员函数,那么程序非良构。
【2】调用类对象
如果 E 在函数调用表达式 E(args) 中拥有类型 cv T,那么
- 在表达式 (E).operator() 的语境中,对名字进行 operator() 的通常查找获得 T 的函数调用运算符,并把每个找到的函数声明添加到候选函数集。
- 对于 T 或 T 的基类中每个(未被隐藏的)非 explicit 的用户定义转换函数,且其 cv 限定符与 T 的 cv 限定符相同或更多,并且该转换函数转换到:
- 函数指针
- 函数指针的引用
- 函数的引用
那么将一个拥有独有名称的代表调用函数添加到候选函数集,该函数的首个形参为转换结果,剩余各形参为转换结果所接受的形参列表,而其返回类型为转换结果的返回类型。如果后继的重载决议选择此代表函数,那么将调用用户定义转换函数,然后调用转换的结果。
任何情况下,就重载决议而言的实参列表,是函数调用表达式的实参列表,前面加上隐含对象实参 E(匹配到代表函数时,用户定义转换将自动将隐含对象实参转换为代表函数的首个实参)。
//
int f1(int){return 1;};
int f2(float){return 2.0;};
struct AStruct
{
using fp1 = int(*)(int);
operator fp1() { return f1; } // 转换到函数指针的转换函数
using fp2 = int(*)(float);
operator fp2() { return f2; } // 转换到函数指针的转换函数
} a;
//
int i = a(1); // 通过转换函数返回的指针调用 f1
【3】调用重载运算符
如果表达式中某个运算符的至少一个实参具有类类型或枚举类型,那么内建运算符和用户定义的运算符重载都参与重载决议,所选择的候选函数集如下:
对于实参具有类型 T1(移除 cv 限定后)的一元运算符 @,或左操作数具有类型 T1 而右操作数具有类型 T2(移除 cv 限定后)的二元运算符 @,准备下列候选函数集:
- 成员候选:如果 T1 是完整类或当前正在定义的类,那么成员候选集是对 T1::operator@ 进行有限定的名字查找的结果。所有其他情况下,成员候选集为空。
- 非成员候选:对于运算符重载容许非成员形式的运算符,为在表达式的语境中对 operator@ 进行无限定名字查找(可能涉及 ADL)所找到的所有声明,但忽略成员函数声明而且其不会阻止到下个外围作用域中继续进行查找。如果二元运算符的两个操作数,或一元运算符的唯一操作数具有枚举类型,那么只查找有形参具有该枚举类型(或到该枚举类型引用)的函数,成为非成员候选函数。
- 内建候选:对于 operator,、一元 operator& 和 operator->,内建候选集为空。对于其他运算符,内建候选是内建运算符页面中列出的函数,只要所有操作数都能隐式转换为其各个形参。如果有任何内建候选拥有的形参列表与某个并非函数模板特化的非成员候选相同,那么该内建候选不会添加到内建候选列表。当考虑内建的赋值运算符时,限制从其左侧实参进行的转换:不考虑用户定义转换。
- 重写候选,:
- 于四个关系运算符表达式 x<y 、 x<=y 、 x>y 及 x>=y ,添加所有找到的成员、非成员及内建 operator<=> 到集合。
- ◦对于四个关系运算符表达式 x<y 、 x<=y 、 x>y 及 x>=y 还有三路比较运算符表达式 x<=>y ,对每个找到的成员、非成员及内建 operator<=> 添加两个形参顺序相反的对应合成候选。
- 对于 x!=y ,添加所有找到的成员、非成员及内建 operator== 到集合。
- 对于相等运算符表达式 x==y 与 x!=y ,对每个找到的成员、非成员及内建 operator== 添加两个个形参顺序相反的对应合成候选。
C++20 起,所有情况下,在重写表达式的语境中不考虑重写候选。对于所有其他运算符,重写候选集为空。
提交给重载决议的候选函数集合是以上集合的并集。就重载决议而言的实参列表由运算符的各操作数组成,除了 operator-> 的情况,其第二操作数并非函数调用的实参。
struct A {
operator int(); // 用户定义转换
};
A operator+(const A&, const A&)// 非成员用户定义运算符
{
//code
};
void m()
{
A a, b;
a + b; // 成员候选:无
// 非成员候选:operator+(a,b)
// 内建候选:int(a) + int(b)
// 重载决议选择 operator+(a,b)
};
如果重载决议选择了内建候选,那么从类类型的操作数进行的用户定义转换序列不允许拥有第二个标准转换序列,用户定义转换函数必须直接给出期待的操作数类型:
struct Y { operator int*(); }; // Y 可转换到 int*
int *a = Y() + 100.0; // 错误:指针和 double 之间没有 operator+
1)对于 operator,、一元 operator& 和 operator->,如果候选函数集中没有可行函数(见后述),那么将运算符解释为内建运算符。
2)如果对运算符 @ 的重载决议选择了重写的 operator<=> 候选,那么用重写的 operator<=> 候选将 x @ y 解释为重写的表达式:当所选择的候选是具有逆序形参的合成候选时,解释为 0 @ (y <=> x),否则为 (x <=> y) @ 0。
3)如果对运算符(为 == 或 != )的重载决议选择了重写 operator== 候选,那么它的返回类型必须是(可有 cv 限定的) bool ,并使用选择的重写 operator== 候选解释 x @ y 为重写表达式:如果选择的候选为拥有逆序形参的合成候选则为 y == x 或 !(y == x) ,否则为 !(x == y) 。
那么内建运算符和用户定义的运算符重载都参与重载决议,这种情况下的重载决议有一条决胜规则:偏好非重写候选甚于重写候选,且偏好非合成重写候选甚于合成重写候选。对于具有逆序实参的查找使得可以只写 operator<=>(std::string, const char*) 与 operator==(std::string, const char*) 就生成 std::string 和 const char* 间的所有双向比较。
【4】调用构造函数初始化
当对类类型的对象进行直接初始化或在复制初始化之外的语境中进行默认初始化时,候选函数是正在初始化的类的所有构造函数。实参列表是初始化器的表达式列表。当对类类型对象从某个相同或派生类类型的对象进行复制初始化,或在复制初始化语境中进行默认初始化时,候选函数是正在初始化的类的所有转换构造函数。实参列表是初始化器的表达式。
1)通过转换进行复制初始化
如果类类型对象的复制初始化要求调用某个用户定义转换以将 cv S 类型的初始化器表达式转换为正在初始化的对象的 cv T 类型,那么下列函数是候选函数:
- T 的所有转换构造函数
- 从 S 及其各基类(除非隐藏)到 T 或 T 的派生类或到它们的引用的非 explicit 转换函数。如果此复制初始化是 cv T 的直接初始化序列的一部分(对于接受一个到 cv T 的引用的构造函数,初始化要绑定到其首个形参的引用),那么也会考虑 explicit 转换函数。
无论哪种方式,就重载决议而言的实参列表均由单个实参组成,即初始化器表达式,它将会与构造函数的首个实参或转换函数的隐式对象实参相比较。
2)通过转换进行非类初始化
当非类类型 cv1 T 对象的初始化要求某个用户定义转换函数,以从类类型 cv S 的初始化器表达式转换时,下列函数为候选:
- S 及其基类(除非隐藏)中的,产生 T 类型,或可由标准转换序列转换到 T 的类型,或到这些类型的引用的非 explicit 用户定义转换函数。对于选择候选函数而言,忽略返回类型上的 cv 限定符。
- 如果这是直接初始化,那么也会考虑 S 及其基类(除非隐藏)中的,产生 T 类型,或可由限定性转换转换到 T 的类型,或到这些类型的引用的 explicit 用户定义转换函数。
无论哪种方式,就重载决议而言的实参列表均由单个实参组成,即初始化器表达式,它将会与转换函数的隐含对象实参相比较。
3)通过转换进行引用初始化
在将指代 cv1 T 的引用绑定到从初始化器表达式转换到类类型 cv2 S 的左值或右值结果的引用初始化期间,为候选集选择下列函数:
- S 及其基类(除非隐藏)中的到以下类型的非 explicit 用户定义转换函数:
- (当初始化左值引用或到函数的右值引用时)到 cv2 T2 的左值引用
- (当初始化右值引用或到函数的左值引用时)cv2 T2 或到 cv2 T2 的右值引用
其中 cv2 T2 与 cv1 T 引用兼容◦对于直接初始化,如果 T2 与 T 类型相同或能以限定性转换转换到 T,那么也会考虑 explicit 用户定义转换函数。
无论哪种方式,就重载决议而言的实参列表均由单个实参组成,即初始化器表达式,它将会与转换函数的隐含对象实参相比较。
4)列表初始化
当非聚合类类型 T 的对象进行列表初始化时,进行两阶段的重载决议。
- 在阶段 1,候选函数是 T 的所有初始化器列表构造函数,而就重载决议而言的实参列表由单个初始化器列表实参组成
- 如果阶段 1 的重载决议失败则进入阶段 2,其中候选函数是 T 的所有构造函数,而就重载决议而言的实参列表由初始化器列表的各个单独元素所组成。
如果初始化器列表为空而 T 拥有默认构造函数,那么跳过阶段 1。
在复制列表初始化中,如果阶段 2 选择 explicit 构造函数,那么初始化非良构(与复制初始化的总体相反,它们甚至不考虑 explicit 构造函数)。
5)可行函数与最佳可行函数
给定以上述方式构造的候选函数集,重载决议的下一步骤是检验各个实参与形参,并将集合缩减为可行函数(viable function)的集合,然后对所有可行函数进行这些逐对比较。如果刚好有一个可行函数优于所有其他函数,那么认为该函数是最佳可行函数,重载决议成功并调用该函数。否则编译失败。
【5】隐式转换序列的排行
重载决议所考虑的实参-形参隐式转换序列与复制初始化中(对于非引用形参)所用的隐式转换对应,但在到隐含对象形参或到赋值运算符的左侧操作数的转换时不考虑创建临时对象的转换。
每种标准转换序列的类型都被赋予三个等级之一:
- 准确匹配:不要求转换、左值到右值转换、限定性转换、函数指针转换、 (C++17 起)类类型到相同类的用户定义转换
- 提升:整型提升、浮点提升
- 转换:整型转换、浮点转换、浮点整型转换、指针转换、成员指针转换、布尔转换、派生类到其基类的用户定义转换
标准转换序列的等级是其所含的标准转换(至多可有三次转换)中的最差等级。
【6】列表初始化中的隐式转换序列
在列表初始化中,实参是 花括号初始化器列表,但它不是表达式,所以到就重载决议而言的形参类型的隐式转换序列一系列规则决定的:
- 如果形参类型是某聚合体 X 且初始化器列表确切地由一个同类型或其派生类(可有 cv 限定)的元素组成,那么隐式转换序列是将该元素转换到形参类型所要求的序列。
- 否则,如果形参类型是到字符数组的引用且初始化器列表拥有单个元素,元素为类型适当的字符串字面量,那么隐式转换序列为恒等转换。
- 否则,如果形参类型是 std::initializer_list<X> 且存在从每个初始化器列表元素到 X 的非窄化隐式转换,那么就重载决议而言的隐式转换序列是所需的最坏转换。如果 花括号初始化器列表 为空,那么转换序列为恒等转换。
- 否则,如果形参类型是“ N 个 T 的数组”(这只对到数组的引用发生),那么初始化器列表必须有 N 个或更少的元素,且所用的隐式转换序列是将列表(或空花括号对,如果 {} 小于 N)的每个元素转换到 T 所需的最坏隐式转换序列。
- 否则,如果形参类型是“ T 的未知边界数组”(这只对到数组的引用发生),那么所用的隐式转换序列是将列表的每个元素转换到 T 所需的最坏隐式转换序列。 (C++20 起)
- 否则,如果形参类型为非聚合类类型 X,那么重载决议选取 X 的构造函数 C 以从实参初始化器列表初始化,如果有多个构造函数可行,但是没有一个优于其他所有构造函数,那么隐式转换序列是有歧义的转换序列。
- 否则,如果形参类型是可按照聚合初始化从初始化器列表初始化的聚合体,那么隐式转换序列是以恒等转换为第二标准转换序列的用户定义转换序列。
- 否则,如果形参是引用,那么应用引用初始化规则。
- 否则,如果形参类型不是类且初始化器列表拥有一个元素,那么隐式转换序列为将该元素转换到形参类型所要求者。
- 否则,如果形参类型不是类且初始化器列表没有元素,那么隐式转换序列为恒等转换。
7、附录-演示代码
编译指令g++ main.cpp test*.cpp - o test.exe -std=c++11。
test.h
#ifndef _TEST_0_H_
#define _TEST_0_H_
#include "test1.h"
#include <iostream>
class ATest
{
public:
void test()
{
Obj o_org = 100;
Obj o_cpy = 10;
std::cout << "o_org = " << o_org << "\n";
o_cpy = o_org;
std::cout << "o_cpy = " << o_cpy << "\n";
};
};
struct DataS
{
int ival;
char cval;
float fval;
};
class DataC
{
private:
/* data */
int ival;
char cval;
float fval;
public:
DataC(/* args */);
~DataC();
};
DataC::DataC(/* args */)
{
}
DataC::~DataC()
{
}
class DataChild : public DataC
{
public:
DataChild& operator=(DataChild&& ) noexcept = default; //
private:
int val;
DataS ds;
};
//
int f1(int){return 1;};
int f2(float){return 2.0;};
struct AStruct
{
using fp1 = int(*)(int);
operator fp1() { return f1; } // 转换到函数指针的转换函数
using fp2 = int(*)(float);
operator fp2() { return f2; } // 转换到函数指针的转换函数
} a;
// int i = a(1); // 通过转换函数返回的指针调用 f1
//
struct A {
operator int(); // 用户定义转换
};
A operator+(const A&, const A&)// 非成员用户定义运算符
{
//code
};
void m()
{
A a, b;
a + b; // 成员候选:无
// 非成员候选:operator+(a,b)
// 内建候选:int(a) + int(b)
// 重载决议选择 operator+(a,b)
};
#endif //_TEST_0_H_
test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_
#include <istream>
#include <ostream>
class Obj
{
public:
Obj(int val_=0);
~Obj();
operator int() const;
int operator()();
int operator()(int val_);
explicit operator bool() const;
// Obj& operator=(const Obj& rhs); //一元操作符,赋值运算=
// Obj& operator=(const Obj&&);
// Obj& operator=(const Obj&) = default; //inline隐式定义
Obj& operator=(const int& rhs); //一元操作符,隐式赋值运算=
Obj& operator=(const char& rhs); //一元操作符,隐式赋值运算=
Obj& operator+=(const Obj& rhs);//一元操作符+=
Obj& operator-=(const Obj& rhs);//一元操作符-=
//
Obj operator++();//一元操作符++,前缀式操作符
Obj operator--();//一元操作符--,前缀式操作符
Obj operator++(int rhs);//一元操作符++,后缀式操作符
Obj operator--(int rhs);//一元操作符--,后缀式操作符
//
static int cmp_Obj(const Obj &obj1, const Obj &obj2);
friend Obj operator+(const Obj&, const Obj&);//二元操作符+
friend Obj operator-(const Obj&, const Obj&);//二元操作符-
friend std::ostream& operator<<(std::ostream&, const Obj&);//io运算操作符<<
friend std::istream& operator>>(std::istream& in, Obj&);//io运算操作符>>
friend class ATest;
private:
// Obj& operator=(const Obj& rhs){return *this;}; //一元操作符,赋值运算=,不做任何处理
Obj& operator=(const Obj&);
// Obj& operator=(const Obj&) = default;
// Obj& operator=(Obj&) = default;
// public:
// Obj& operator=(const Obj&) = delete;
// Obj& operator=(Obj&) = delete;
private:
int val;
};
inline bool operator==(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) == 0; }
inline bool operator!=(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) != 0; }
inline bool operator>=(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) >= 0; }
inline bool operator<=(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) <= 0; }
inline bool operator>(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) > 0; }
inline bool operator<(const Obj& obj1, const Obj& obj2) { return Obj::cmp_Obj(obj1, obj2) < 0; }
#endif //_TEST_1_H_
test1.cpp
#include "test1.h"
Obj::Obj(int val_) : val(val_)
{
};
Obj::~Obj()
{
};
Obj::operator int() const
{
return val;
}
int Obj::operator()()
{
return val;
}
int Obj::operator()(int val_)
{
return val_<0?-val_:val_;
}
Obj::operator bool() const
{
return 0!=val;
}
Obj& Obj::operator=(const Obj& rhs)
{
if(this==&rhs)
return *this;
val = rhs.val;
return *this;
};
Obj& Obj::operator=(const int& rhs)
{
val = rhs;
return *this;
};
Obj& Obj::operator=(const char& rhs)
{
val = rhs;
return *this;
};
Obj& Obj::operator+=(const Obj& rhs)
{
val += rhs.val;
return *this;
}
Obj& Obj::operator-=(const Obj& rhs)
{
val -= rhs.val;
return *this;
}
Obj Obj::operator++()
{
++val;
return *this;
}
Obj Obj::operator--()
{
--val;
return *this;
}
Obj Obj::operator++(int rhs)
{
Obj ret(*this);
++*this;
return ret;
}
Obj Obj::operator--(int rhs)
{
Obj ret(*this);
--*this;
return ret;
}
int Obj::cmp_Obj(const Obj &obj1, const Obj &obj2)
{
return obj1.val - obj2.val;
}
Obj operator+(const Obj& lhs, const Obj& rhs)
{
Obj obj(lhs);
obj.val+=rhs.val;
return obj;
};
Obj operator-(const Obj& lhs, const Obj& rhs)
{
Obj obj(lhs);
obj.val-=rhs.val;
return obj;
};
std::ostream& operator<<(std::ostream& os, const Obj& obj)
{
os << obj.val;
return os;
};
std::istream& operator>>(std::istream& in, Obj& obj)
{
//最好安全检查
in >> obj.val; //取出数据写入val
return in;
}
test2.h
#ifndef _TEST_2_H_
#define _TEST_2_H_
class ObjF
{
public:
ObjF& operator=(const ObjF& rhs); //
ObjF& operator=(const ObjF&& ); //
ObjF& operator=(ObjF&& ) noexcept = default; //
// ObjF& operator=(ObjF&& ); //
ObjF& operator=(const float&& ); //
ObjF& operator=(float&& ); //
private:
float val;
};
#endif //_TEST_2_H_
test2.cpp
#include "test2.h"
#include <iostream>
#include <utility>
ObjF& ObjF::operator=(const ObjF& rhs) //复制赋值
{
if(this==&rhs)
return *this;
val = rhs.val;
std::cout << "copy operator= call\n";
return *this;
};
ObjF& ObjF::operator=(const ObjF&& rhs)//移动赋值
{
val = std::move(rhs.val);
std::cout << "move operator= call1\n";
return *this;
};
// ObjF& ObjF::operator=(ObjF&& rhs)//移动赋值
// {
// val = std::move(rhs.val);
// std::cout << "move operator= call2\n";
// return *this;
// };
ObjF& ObjF::operator=(const float&& rhs)//移动赋值
{
val = std::move(rhs);
std::cout << "move operator= call3\n";
return *this;
};
ObjF& ObjF::operator= (float&& rhs)//移动赋值
{
val = std::move(rhs);
std::cout << "move operator= call4\n";
return *this;
};
test3.h
#ifndef _TEST_3_H_
#define _TEST_3_H_
void funcAddr_test();
#endif //
test3.cpp
#include "test3.h"
int f(int) { return 1; }
int f(double) { return 2; }
void g( int(&f1)(int), int(*f2)(double) ) {}
template< int(*F)(int) >
struct Templ {};
struct Foo {
int mf(int) { return 3; }
int mf(double) { return 4; }
};
struct Emp {
void operator<<(int (*)(double)) {}
};
void funcAddr_test()
{
// 1. 初始化
int (*pf)(double) = f; // 选择 int f(double)
int (&rf)(int) = f; // 选择 int f(int)
int (Foo::*mpf)(int) = &Foo::mf; // 选择 int mf(int)
// 2. 赋值
pf = nullptr;
pf = &f; // 选择 int f(double)
// 3. 函数实参
g(f, f); // 为第一实参选择 int f(int)
// 为第二实参选择 int f(double)
// 4. 用户定义运算符
Emp{} << f; // 选择 int f(double)
// 5. 返回值
auto foo = []() -> int (*)(int) {
return f; // 选择 int f(int)
};
// 6. 转型
auto p = static_cast<int(*)(int)>(f); // 选择 int f(int)
// 7. 模板实参
Templ<f> t; // 选择 int f(int)
}
main.cpp
#include "test.h"
#include "test1.h"
#include "test2.h"
#include "test3.h"
int main(int argc, char* argv[])
{
int a = 0;
int b = {};
int c = int{};
int d = {0};
int e = int{0};
int f = int();
int g = int(0);
Obj oa = {0};
Obj ob = Obj{0};
Obj oc = {};
Obj od = Obj{};
//
int a1 = 10;
a1 *=1.0;
a1 /='a';
a1 -= true;
//
// Obj o_org = 100;
// Obj o_cpy = 10;
// std::cout << "o_org = " << o_org << "\n";
// o_cpy = o_org;
// std::cout << "o_cpy = " << o_cpy << "\n";
ATest at;
at.test();
//
DataS d1,d2;
d2 = d1;
DataC c1, c2;
c2 = c1;
//
ObjF of1,of2,of3;
of2 = of1;
of3 = std::move(of1);
//
DataS d3,d4;
d4 = std::move(d3);
DataC c3, c4;
c4 = std::move(c3);
//
DataChild dc1,dc2;
dc2 = std::move(dc1);
//
funcAddr_test();
//
m();
return 0;
};