C++_Primer14.overload_type_conversion

操作重载和类型转换

基本概念

重载的运算符由关键字 operator 和其后要定义的运算符号共同组成。

重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。
对于二元运算符,左侧运算对象传递给第一个参数,右侧传递给第二个。
除了重载的函数调用运算符 operator() 之外,其他重载运算符不能含有默认实参。

如果一个运算符函数是成员函数,则它的第一个运算对象(左侧)绑定到隐式的this指针上,因此,成员运算符函数的参数(显式参数)数量比运算符的运算对象总数少一个。

运算符函数要么是类的成员,要么至少含有一个类类型的参数。

// 错误:不能为int定义内置的运算符(不能重载内置类型的运算符)
int operator+(int, int);

只能重载已有运算符,无权发明新的运算符。比如,不能提供 operator** 来执行幂操作。

有四个符号(+ - * &)既是一元运算符也是二元运算符,从参数的数量可以推断到底定义的是哪种运算符。

重载的运算符优先级和结合率与对应的内置运算符保持一致。

可以被重载的运算符:

+   -   ×   /   %   ^
&   |   ~   !   ,   =
<   >   <=  >=  ++  --
<<  >>  ==  !=  &&  ||
++  -=  /=  %=  ^=  &=
|=  *=  <<= >>= []  ()
->  ->* new new[]   delete  delete[]

不能被重载:

::  .*  .   ?:

可以直接调用运算符函数

data1 + data2;
operator+(data1, data2);

如果运算符函数是成员函数:

data1 += data2;
data1.operator+=(data2);

某些运算符不应该被重载

逻辑与运算符、逻辑或运算符和都好运算符的运算对象求值顺序规则无法保留下来;
&& 和 || 运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。
当代码使用了这些运算符的重载版本时,用会可能会突然发现他们一直习惯的求值规则不再适用了。
因此不建议重载他们。

还有一个原因不建议重载都好运算符和取地址运算符:
C++语言已经定义了这两种运算符用于类类型对象时的特殊含义,这一点与大多数运算符都不相同。
这两种运算符已经有了内置的含义,所以一般来说他们不应该被重载。

一般不应该重载逗号、取地址、逻辑与和逻辑或运算符

使用与内置类型一致的含义

  • 如果类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致
  • 如果类的某个操作是检查相等性,则定义 operator==;当类有了 operator==,意味着它通常也应该有 operator!=
  • 如果类包含一个内在的单序比较操作,则定义 operator<;相应的,它也应该含有其他关系操作
  • 重载运算符的返回类型通常应该与其内置版本的返回类型兼容
    • 逻辑运算符和关系运算符返回bool
    • 算术运算符返回一个类类型的值
    • 赋值运算符和复合赋值运算符(+=)返回左侧运算对象的引用
  • 如果类含有算术运算符或位运算符,则最好也提供对应的复合赋值运算符

成员还是非成员函数

  • 赋值=,下标[],调用() 和成员访问箭头-> 运算符必须是成员
  • 复合赋值运算符一般来说应该是成员,但不是必须
  • 改变对象状态的运算符或给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员
  • 具有对称性的运算符可能转换任意一端的运算对象,如算术、相等性、关系和位运算符等,他们通常应该是非成员函数

当把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象:

string s = "world";
string t = s + "!";     // 正确
string u = "hi" + s;    // 如果+是string的成员,则产生错误

除非别无选择,一般优先作为普通函数

练习14.2

为 Sales_data 编写重载的输入、输出、加法和复合赋值运算符

// Sales_data.h
#ifndef SALES_DATA_H_
#define SALES_DATA_H_


#include <iostream>
#include <string>

class Sales_data {
    friend std::istream& operator>>(std::istream&, Sales_data&);
    friend std::ostream& operator<<(std::ostream&, const Sales_data&);
    friend Sales_data operator+(const Sales_data&, const Sales_data&);
    friend bool operator==(const Sales_data&, const Sales_data&);
public:
    Sales_data(const std::string& s, unsigned n, double p)
        : bookNo(s), units_sold(n), revenue(n*p) {}
    Sales_data() : Sales_data("", 0, 0.f) {}
    Sales_data(const std::string& s) : Sales_data(s, 0, 0.f) {}
    Sales_data(std::istream&);

    Sales_data& operator+=(const Sales_data&);
    std::string isbn() const { return bookNo; }
private:
    double avg_price() const;

    std::string bookNo;
    unsigned units_sold;
    double revenue = 0.;
};

std::istream& operator>>(std::istream&, Sales_data&);
std::ostream& operator<<(std::ostream&, const Sales_data&);
Sales_data operator+(const Sales_data&, const Sales_data&);
bool operator==(const Sales_data&, const Sales_data&);

#endif

// Sales_data.cpp
#include "Sales_data.h"

std::istream& operator>>(std::istream& is, Sales_data& sd) {
    double price;
    is >> sd.bookNo >> sd.units_sold >> price;
    if (is) {
        sd.revenue = price * sd.units_sold;
    } else {
        sd = Sales_data();
    }
    return is;
}
std::ostream& operator<<(std::ostream& os, const Sales_data& sd) {
    os << sd.bookNo << " " << sd.units_sold << " " << sd.revenue
        << " " << sd.avg_price();
    return os;
}
Sales_data operator+(const Sales_data& lhs, const Sales_data& rhs) {
    Sales_data sd = lhs;
    sd += rhs;
    return sd;
}

bool operator==(const Sales_data& lhs, const Sales_data& rhs) {
    return lhs.bookNo == rhs.bookNo;
}

Sales_data::Sales_data(std::istream& is) {
    is >> * this;
}

Sales_data& Sales_data::operator+=(const Sales_data& rhs) {
    units_sold += rhs.units_sold;
    revenue += rhs.revenue;
    return * this;
}

double Sales_data::avg_price() const {
    return units_sold ? revenue / units_sold : 0.;
}

// Sales_data_test.cpp
#include "Sales_data.h"

int main(int argc, char** argv) {
    Sales_data item(std::cin);
    std::cout << "item:\t" << item << std::endl;

    Sales_data item2(std::cin);
    std::cout << "item2:\t" << item2 << std::endl;
    if (item == item2) {
        std::cout << "item equals to item2" << std::endl;
        Sales_data item3 = item + item2;
        item += item2;
        std::cout << "item:\t" << item << std::endl;
        std::cout << "item3:\t" << item3 << std::endl;
    } else {
        std::cout << "not equal" << std::endl;
    }

    return 0;
}

// makefile(注意,命令前用 tab 开头)
all: Sales_data.o Sales_data_test.o
	g++ --std=c++11 -o test Sales_data_test.o Sales_data.o
o:
	g++ --std=c++11 -c Sales_data.cpp
	g++ --std=c++11 -c Sales_data_test.cpp
clean:
	rm Sales_data.o Sales_data_test.o test
$ make
g++    -c -o Sales_data.o Sales_data.cpp
In file included from Sales_data.cpp:1:0:
Sales_data.h:26:22: warning: non-static data member initializers only available with -std=c++11 or -std=gnu++11
     double revenue = 0.;
                      ^
Sales_data.h: In constructor ‘Sales_data::Sales_data()’:
Sales_data.h:15:41: warning: delegating constructors only available with -std=c++11 or -std=gnu++11
     Sales_data() : Sales_data("", 0, 0.f) {}
                                         ^
Sales_data.h: In constructor ‘Sales_data::Sales_data(const string&)’:
Sales_data.h:16:60: warning: delegating constructors only available with -std=c++11 or -std=gnu++11
     Sales_data(const std::string& s) : Sales_data(s, 0, 0.f) {}
                                                            ^
g++    -c -o Sales_data_test.o Sales_data_test.cpp
In file included from Sales_data_test.cpp:1:0:
Sales_data.h:26:22: warning: non-static data member initializers only available with -std=c++11 or -std=gnu++11
     double revenue = 0.;
                      ^
Sales_data.h: In constructor ‘Sales_data::Sales_data()’:
Sales_data.h:15:41: warning: delegating constructors only available with -std=c++11 or -std=gnu++11
     Sales_data() : Sales_data("", 0, 0.f) {}
                                         ^
Sales_data.h: In constructor ‘Sales_data::Sales_data(const string&)’:
Sales_data.h:16:60: warning: delegating constructors only available with -std=c++11 or -std=gnu++11
     Sales_data(const std::string& s) : Sales_data(s, 0, 0.f) {}
                                                            ^
g++ --std=c++11 -o test Sales_data_test.o Sales_data.o
$ ./test
abc 3 10
item:	abc 3 30 10
abc 5 9
item2:	abc 5 45 9
item equals to item2
item:	abc 8 75 9.375
item3:	abc 8 75 9.375
$ ./test
abc 3 10
item:	abc 3 30 10
abd 4 9
item2:	abd 4 36 9
not equal

Sales_data.h 中的成员函数的实现不要写在类外,也不要写非成员函数的实现,因为上述几个文件是分开编译的
ifndef 的作用是防止重复引入代码,但仅对同一次翻译有效,编译两次(调用两次g++)的话会引入两次
所以类外实现的函数最好写在cpp文件中

输入和输出运算符

重载输出运算符<<

  • 输出运算符尽量减少格式化操作
    • 通常,输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符
  • 输入输出运算符必须是非成员函数

重载输入运算符>>

  • 输入运算符必须处理输入可能失败的情况,而输出运算符不需要
    • 流含有错误类型的数据时读取操作可能失败
    • 读取操作达到末尾或遇到输入流的其他错误时也会失败
    • 读完流后通常要进行判断:if(is)
  • 读取发生错误时,输入运算符应该负责从错误中恢复

算术和关系运算符

相等运算符

  • 如果类含有判断两个对象是否相等的操作,应该把函数定义为 operator== 而非一个普通的命名函数
    • 用户不必再费时费力学习和记忆一个全新的函数名字
    • 定义了==运算符后也更容易使用标准库容器和算法
  • 相等运算符应该具有传递性,即如果 a==bb==c 都为真,则 a==c 也应该为真
  • 如果定义了operator==,则这个类也应该定义operator!=
  • 相等运算符和不相等运算符中的一个应该把工作委托给另一个,意味着其中一个运算符负责实际的比较工作,而另一个只是调用它

关系运算符

  • 顺序关系应当与关联容器中对关键字的要求一致
  • 如果类同时含有==运算符的话,则定义一种关系令其与==保持一致
    • 如果两个对象是!=的,那么一个对象应该<另外一个

赋值运算符

拷贝赋值和移动赋值运算符
赋值运算符都必须定义为成员函数

除了接受同类型参数外,还可接受列表:

class StrVec {
    StrVec& operator=(std::initializer_list<std::string>);
    // ...
};
StrVec& StrVec::operator=(std::initializer_list<std::string> il) {
    auto data = alloc_n_copy(il.begin(), il.end());
    free();
    elements = data.first;
    first_free = cap = data.second;
    return * this;
}

// 使用:
StrVec<std::string> v;
v = {"a", "an", "the"};

这个运算符无需检查参数是否指向自身,因为形参 initializer_list 与 this 指向的不是同一个对象。

复合赋值运算符

复合赋值运算符不非得是类的成员,但一般倾向于把包括复合赋值在内的所有赋值运算都定义在类的内部

下标运算符

  • operator[] 下标运算符必须是成员函数
  • 下标运算符通常返回所访问元素的引用
    • 这样做的好处是下标可以出现在赋值运算符的任意一端
    • 最好同时定义下标运算符的常量版和非常量版(const 和 非const)
class StrVec {
public:
    std::string& operator[](std::size_t n) {
        return elements[n];
    }
    const std::string& operator[](std::size_t n) const {
        return elements[n];
    }
private:
    std::string* elements;
};

// 使用例:
const StrVec cvec = svec;
if (svec.size() && svec[0].empty()) {
    svec[0] = "zero";       // 正确,使用的非const版本
    cvec[0] = "zero";       // 错误,使用的const版本
}

递增和递减运算符

class StrBlobPtr {
public:
    StrBlobPtr& operator++();       // 前置
    StrBlobPtr& operator--();
    StrBlobPtr& operator++(int);    // 后置,一般不会用到形参,无需为其命名
    StrBlobPtr& operator--(int);
private:
    size_t curr;
};

StrBlobPtr& StrBlobPtr::operator++() {
    // 检查是否指向容器末尾
    check(curr, "increment past end of StrBlobPtr");
    ++curr;
    return * this;
}
StrBlobPtr& StrBlobPtr::operator--() {
    --curr;
    // 检查是否指向容器开头。如果指向开头,则--curr会变成一个非常大的正数值
    check(curr, "decrement past begin of StrBlobPtr");
    return * this;
}
StrBlobPtr& StrBlobPtr::operator++(int) {
    // 此处无需检查有效性,调用前置运算时才需要检查
    StrBlobPtr ret = *this;
    ++*this;
    return ret;
}
StrBlobPtr& StrBlobPtr::operator--(int) {
    // 此处无需检查有效性,调用前置运算时才需要检查
    StrBlobPtr ret = *this;
    --*this;
    return ret;
}

显式调用后置运算符

StrBlobPtr p(a1);
p.operator++(0);    // 调用后置版本
p.operator++();     // 调用前置版本

成员访问运算符

解引用和箭头运算符
箭头运算符必须是类的成员。解引用运算符通常也是,但不必须

class StrBlobPtr {
public:
    std::string& operator*() const {
        auto p = check(curr, "dereference past end");
        return (*p)[curr];
    }
    std::string* operator->() const {
        // 委托给解引用运算符
        return & this->operator*();
    }
};

对于箭头运算符,比如 point->mem 的执行过程如下:

  • 1, 如果 point 是指针,则我们应用内置的箭头运算符,表达式等价于 (*point).mem
  • 2, 如果point是定义了 operator-> 的类的一个对象,则我们使用 point.operator->() 的返回结果来获取 mem
    • 如果返回结果是一个指针,则执行第一步;如果该结果本身含有重载的 operator->(),则重复调用当前步骤

函数调用运算符

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象,该类的对象称为函数对象

struct absInt {
    int operator()(int val) const {
        return val < 0 ? -val : val;
    }
};

// 使用例:
int i = -42;
absInt absObj;
int ui = absObj(i);

函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别

含有状态的函数对象类

class PrintString {
public:
    PrintString(ostream& o = cout, char c = ' '):
        os(o), sep(c) {}
    void operator()(const string& s) const { os << s << sep; }
private:
    ostream& os;
    char sep;
};

// 使用例:
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));

for_each 第三个实参是类型 PrintString 的一个临时对象(假设叫tmp),其中用 cerr 和换行符初始化了该对象。当程序调用 for_each 时(会依次调用tmp(vs[i])),将会把 vs 中的每个元素依次打印到 cerr 中,元素间用换行符分割。

lambda 是函数对象

当我们编写了一个 lambda 后,编译器将其翻译成一个未命名类的未命名对象。在这个类中含有一个重载的函数调用运算符:

stable_sort(words.begin(), words.end(),
        [](const string& a, const string& b)
            { return a.size() < b.size(); });

// 其行为类似于:
class ShorterString {
public:
    bool operator()(const string& a, const string& b) const {
        return s1.size() < s2.size();
    }
};
stable_sort(words.begin(), words.end(), ShorterString());

对于需要捕获的变量,需要为其建立相应的成员变量:

auto wc = find_if(words.begin(), words.end(),
        [sz](const string& a)
            { return a.size() >= sz; });

// lambda 表达式产生的类相当于:
class SizeComp {
    SizeCOmp(size_t n) : sz(n) {}
    bool operator()(const string& s) const {
        return s.size() >= sz;
    }
private:
    size_t sz;
};
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));

标准库定义的函数对象

functional 头文件

标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。比如plus类定义了一个函数调用运算符用于对一对运算对象执行+操作;modulus类定义了二元的%操作;equal_to执行==等等。
他们都被定义成呢个模板的形式:

plus<int> intAdd;           // 加法
negate<int> intNegate;      // 对int取反
int sum = intAdd(10, 20);           // 30
sum = intNegate(intAdd(10, 20));    // -30
sum = intAdd(10, intNegate(10));    // 0
算术 关系 逻辑
plus equal_to logical_and
minus not_equal_to logical_or
multiplies greater logical_not
divides greater_equal
modulus less
negate less_equal
// 降序排序,svec是一个 vector<string>
sort(svec.begin(), svec.end(), greater<string>());

vector<string*> nameTable;
// 错误:nambeTable中的指针彼此没有关系,所以<将产生未定义的行为
sort(nameTable.begin(), nameTable.end(),
        [](string* a, string* b) { return a < b; });
// 正确:标准库规定指针的less是定义良好的
sort(nameTable.begin(), nameTable.end(), less<string*>());

函数对象例(练习):

template<typename T>
class Add {
    T operator()(T a, T b) { return a + b; }
}

int a = Add<int>()(5, 6);

int m(int a, int n) {
    if (n == 1) {
        return a;
    }
    return m(a, n/2) * m(a, n/2) * (n%2==0 ? 1 : a);
}

练习14.42

  1. 统计大于1024的值有多少个
  2. 找到第一个不等于 pooh 的字符串
  3. 将所有的值乘以2
#include <vector>
#include <algorithm>
#include <string>
#include <functional>
#include <iostream>

using std::placeholders::_1;

void test1();
void test2();
void test3();

int main(int argc, char** argv) {
    test1();
    test2();
    test3();

    return 0;
}

void test1() {
    std::vector<int> v{1000,2000,3000,4000};
    std::cout << "greater than 1024: " << std::count_if(v.begin(), v.end(),
            std::bind(std::greater<int>(), _1, 1024)) << std::endl;
}
void test2() {
    std::vector<std::string> v{"pooh", "booh", "pooj", "hello", "world"};
    auto s = std::find_if(v.begin(), v.end(),
            std::bind(std::not_equal_to<std::string>(), _1, "pooh"));
    std::cout << "first not equal to \"pooh\": " << *s << std::endl;
}
void test3() {
    std::vector<double> v{2.4,5.6,7.8,100.06};
    std::transform(v.begin(), v.end(), v.begin(), std::bind(std::multiplies<double>(), _1, 2.));
    for_each(v.begin(), v.end(), [](double e) { std::cout << e << std::endl; });
}

输出:

$ g++ --std=c++11 -o test test.cpp
$ ./test
greater than 1024: 3
first not equal to "pooh": booh
4.8
11.2
15.6
200.12

练习14.43

使用标准库函数对象判断一个给定的int值是否能被int容器中的所有元素整除。

#include <vector>
#include <functional>
#include <iostream>
#include <algorithm>

using std::placeholders::_1;

int main(int argc, char** argv) {
    int a = 2*3*5*7;
    std::vector<int> v{2,3,5,7};
    std::modulus<int> mod;

    // all_of: 是否全都满足; any_of: 是否有满足的; none_of: 是否都不满足
    auto f = std::all_of(v.begin(), v.end(), [mod, a](int e) { return 0 == mod(a, e); });
    std::cout << f << std::endl;
    if (!f) {
        std::cout << "not divisible" << std::endl;
    } else {
        std::cout << "divisible" << std::endl;
    }

    return 0;
}

可调用对象与function

C++中的可调用对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类

不同的类型可能具有相同的调用形式

int add(int i, int j) { return i + j; }
auto mod = [](int i, int j) { return i % j; };
struct divide {
    int operator()(int denominator, int divisor) {
        return denominator / divisor;
    }
};

上述可调用对象都有相同的调用形式:

int(int, int)

虽然具有相同的调用形式,但他们不是同一种类型,无法放在同一个容器中。比如要方便地使用他们,想要把他们放在一个map中,通过不同的符号调用不同的对象:

map<string, int(*)(int, int)> binops;
binops.insert({"+", add});      // 正确
binops.insert({"%", mod});      // 错误:mod不是一个函数指针

标准库function类型

function 定义在 functional 头文件中

操作 说明
function f; f是一个用来存储可调用对象的空function,他们的调用形式应该与函数类型T相同(retType(args))
function f(nullptr); 显式地构造一个空function
function f(obj); 在f中存储可调用对象obj的副本
f 将f作为条件:当f含有一个可调用对象时为真;否则为假
f(args) 调用f中的对象,参数是args

定义为 function的成员的类型:

类型 说明
result_type 该function类型的可调用对象返回的类型
argument_type
first_argument_type
second_argument_type
当T有一个或两个实参时定义的类型。如果T只有一个实参,
则argument_type是该类型的同义词;如果T有两个实参,
则first_argument_type和second_argument_type
分别代表两个实参的类型

这样就可以把不同类型的可调用对象加入到同一个容器中了:

function<int(int, int)> f1 = add;
function<int(int, int)> f2 = divide();
function<int(int, int)> f3 = [](int i, int j) { return i * j; };
cout << f1(4, 2) << endl;

map<string, function<int(int, int)>> binops = {
    {"+", add},
    {"-", std::minus<int>()},
    {"/", divide()},
    {"*", [](int i, int j) { return i * j; }},
    {"%", mod},
};

binops["+"](10, 5);
binops["/"](10, 5);

重载的函数(二义性)

我们不能直接将重载函数的名字存入 function 类型的对象中:

int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert({"+", add});      // 歧义
// 正确的做法:
int (*fp)(int, int) = add;      // fp指向的是两个int参数的add函数
binops.insert({"+", fp});
// 用lambda消除二义性:
binops.insert({"+", [](int a, int b) { return add(a, b); }});

重载、类型转换与运算符

类类型的类型转换

转换构造函数和类型转换运算符共同定义了类类型转换(class-type conversion),也称为用户定义的类型转换

类型转换运算符

class SmallInt {
public:
    // 既定义了int向SmallInt 的转换,也定义了SmallInt 向 int 的转换。
    SmallInt(int i = 0) : val(i) {
        if (i < 0 || i > 255) {
            throw std::out_of_range("Bad SmallInt value");
        }
    }
    operator int() const { return val; }
private:
    std::size_t val;
};

SmallInt si;
si = 4;         // 首先将4隐式转换成SmallInt, 然后调用SmallInt::operator=
si + 3;         // 先将si转换成int,然后再执行加法
SmallInt s1 = 3.14  // 将double转换成int,在调用 SmallInt(int) 构造函数
si + 3.14;      // SmallInt 转换成 int,再继续转换成 double

显式类型转换运算符

class SmallInt {
public:
    explicit operator int() const { return val; }
    // ...
};

SmallInt si = 3;    // 正确,SmallInt 等构造函数不是显式的
si + 3;             // 错误
static_cast<int>(si) + 3;   // 正确,显式地调用类型转换

显式类型转换一般只能通过显式等强制类型转换才可以,但有一个例外,即表达式被用作条件时,编译器会将显式的类型转换自动应用于它。(if,while,for,逻辑运算,三目条件运算)

练习14.47

说明下面两个类型转换运算符等区别

struct Integer {
    operator const int();
    operator int() const;
};

第一个是 const int 类型转换符,适合于接受常量int参数的地方;第二个是 int 类型转换,并且不允许修改对象的内容。

避免二义性的类型转换

如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式。否则的话,代码具有二义性。

两种情况可能产生多重转换路径:

  • 两个类提供相同等类型转换
    • 当A类定义了一个接受B类对象等转换构造函数,同时B类定义了一个转换目标是A类等类型转换运算符时,我们就说他们提供了相同的类型转换
    • 只能显式地调用:b.operator A(), A(b)
  • 类定义了多个转换规则,而这些转换涉及的类型本身可以通过其他类型转换联系在一起。

通常情况,不要为类定义相同等类型转换,也不要在类中定义两个及两个以上转换源或转换目标时算数类型等转换

c除了显式地向bool类型的转换外,我们应尽量避免定义类型转换函数并尽可能地限制那些“显然正确”等非显式构造函数

重载函数产生等二义性

struct C {
    C(int);
};
struct D {
    D(int);
};
void manip(const C&);
void manip(const D&);
manip(10);      // 二义性错误:manip(C(10)) 还是 manip(D(10))

函数匹配与重载运算符

如果对同一个类既提供了转换目标是算术类型等类型转换,也提供了重载等运算符,则将会遇到重载运算符与内置运算符的二义性问题:

class SmallInt {
    friend SmallInt operator+(const SmallInt&, const SmallInt&);
public:
    SmallInt(int = 0);
    operator int() const { return val; }
private:
    std::size_t val;
};

SmallInt s1, s2;
SmallInt s3 = s1 + s2;  // 使用重载运算符 operator+
int i = s3 + 0;         // 二义性错误

小结

一个重载等运算符必须是某个类的成员或者至少拥有一个类类型等运算对象。重载运算符的运算对象数量、结合律、优先级与对应的用于内置类型的运算符完全一致。当运算符被定义为类的成员时,类对象的隐式this指针绑定到第一个运算对象。赋值、下标、函数调用和箭头运算符必须作为类的成员。

如果类重载了函数调用运算符operator(),则该类等对象被称作“函数对象”。这样的对象常用在标准函数中。lambda表达式是一种简便的定义函数对象类的方式。

在类中可以定义转换源或转换目的的是该类型本身等类型转换,这样等类型转换将自动执行。
只接受单独一个实参的非显式构造函数定义了从实参类型到类类型的类型转换;
非显式等类型转换运算符则定义了从类类型到其他类型的转换。

posted @ 2023-06-12 12:32  keep-minding  阅读(18)  评论(0)    收藏  举报