本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版后一修订版 | 前一修订版 | ||
cs:programming:cpp:cpp_primer:14_overloading [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:cpp_primer:14_overloading [2024/11/13 12:31] (当前版本) – [箭头运算符返回值的限制] codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | | ||
+ | C++ Primer 笔记 第十四章\\ | ||
+ | ---- | ||
+ | C++ 允许用户通过**运算符重载**(// | ||
+ | ====基础概念==== | ||
+ | 重载的运算符实际上是有特殊名字的函数。该函数的名称由两部分组成:关键字 '' | ||
+ | 重载的运算符函数有几个特点: | ||
+ | * **参数数量**需要与bulit-in 运算符相同,也就是运算符重载不能改变运算符的定义 | ||
+ | * 如果重载的运算符函数是**成员函数**,则其 left operand 默认绑定当前对象 '' | ||
+ | 重载运算符有两种形式:是成员函数,或者参数列表中至少包含一个 **class type** 的参数 | ||
+ | 比如下方这样的写法就是错误的: | ||
+ | <code cpp> | ||
+ | // error: cannot redefine the built-in operator for ints | ||
+ | int operator+(int, | ||
+ | </ | ||
+ | |||
+ | ===支持重载的运算符=== | ||
+ | C++ 支持重载的运算符如下图: | ||
+ | \\ \\ | ||
+ | {{ cs: | ||
+ | \\ \\ | ||
+ | 有几点需要注意的是: | ||
+ | * C++ 只允许重载已有的,被允许的运算符。除此之外的自定义运算符是不允许的 | ||
+ | * '' | ||
+ | * 被重载的运算符会**保留**运算符**优先级**与**结合律** | ||
+ | ==直接调用重载运算符== | ||
+ | 由于重载的运算符是函数,因此该重载存在使用符号调用,或者直接调用的两种方式: | ||
+ | <code cpp> | ||
+ | // equivalent calls to a nonmember operator function | ||
+ | data1 + data2; | ||
+ | operator+(data1, | ||
+ | |||
+ | //member function version | ||
+ | data1 += data2; | ||
+ | data1.operator+=(data2); | ||
+ | </ | ||
+ | ===运算符重载的注意事项=== | ||
+ | ==某些运算符不应该被重载== | ||
+ | 如果默认的运算符拥有以下的特性: | ||
+ | * 运算符自身保证了 order of evaluation | ||
+ | * 运算符自身具有 short-circuit evaluation(也就是某个表达式得出结果之前不会执行另外的表达式) | ||
+ | * 运算符对类对象的操作已经被**默认定义** | ||
+ | 则这种运算符**不应该被重载**。比如 ''&&'' | ||
+ | ==重载应该保证定义上的一致性== | ||
+ | 在设计类的运算时,需要考虑两点: | ||
+ | * 类对象是否需要某类运算 | ||
+ | * 该运算在逻辑上与 built-in 类型的运算是否有**一致性** | ||
+ | 比如以下常见的,保证一致性例子: | ||
+ | * 如果类中定义了输入输出,则可以通过重载 shift operator 来与 bulit-in type 统一输入输出的形式 | ||
+ | * 如果类中定义了等价的比较,那么应该重载 '' | ||
+ | * 如果类中定义了单一的自然排序,那么应该重载 ''<'' | ||
+ | 需要注意的是,这种**一致性也包括返回值**;也就是说: | ||
+ | * 逻辑 / 关系运算应该返回 '' | ||
+ | * 算数运算应该返回 class 类型的**值** | ||
+ | * 赋值 / 复合赋值运算应该返回 **left-hand operand** 的**引用** | ||
+ | <WRAP center round box 100%> | ||
+ | 总的来说,重载运算符应该基于默认运算符的意义进行设计。我们可以延伸,映射该意义到类对象上,但不能违背已有的意义。正确的使用重载可以使程序变得更加直观,而滥用只会造成更多的歧义。 | ||
+ | </ | ||
+ | <WRAP center round tip 100%> | ||
+ | 赋值运算符的设计应该与合成的版本类似:赋值之后,左右的值应该相等,且运算应该返回左边对象的引用。如果类中定义了算数运算或者位运算,则我们应该提供对应的复合赋值运算。 | ||
+ | </ | ||
+ | ==成员或者非成员的选择== | ||
+ | 重载的运算符函数是否应该是成员函数取决于该运算符的特性: | ||
+ | * **必须定义为成员函数**的:**赋值** '' | ||
+ | * **必须定义为非成员函数**的:I / O shift operator | ||
+ | * 应该定义为**成员**函数的: | ||
+ | * **复合赋值**运算符 | ||
+ | * 改变对象状态,或与其对象类型联系紧密的运算符,比如**自增** '' | ||
+ | * 应该定义为**非成员**函数的: | ||
+ | * //Symmetric Operators// | ||
+ | 值得提出的是,当重载 Symmetric operators 的时候,需要考虑**类型转换**的问题。这是因为 Symmetric operators 并不强制算子的位置:通常在 Symmetric operators 出现的运算中,左算子与右算子是可以互换位置的。\\ \\ | ||
+ | 另外,如果某个运算符被重载为了**成员函数**,则其算子的位置就**固定**下来了。这里特指左算子,必须是**该重载运算符所在类的对象**: | ||
+ | <code cpp> | ||
+ | string s = " | ||
+ | string t = s + " | ||
+ | string u = " | ||
+ | </ | ||
+ | ====输入输出运算符==== | ||
+ | ===重载输出运算符 <<=== | ||
+ | 输出运算符的重载对参数与返回值有如下的要求: | ||
+ | * 第一个参数应该是 non-const 的 '' | ||
+ | * Non-const 是因为写入流会改变当前对象的状态 | ||
+ | * 引用是因为 '' | ||
+ | * 第二个参数应该是 **reference to const of the class type**:const 是因为打印不会改变当前对象的内容 | ||
+ | * 返回值应该是 non-const 的 '' | ||
+ | // | ||
+ | <code cpp> | ||
+ | ostream & | ||
+ | { | ||
+ | os << item.isbn() << " " << item.units_sold << " " | ||
+ | << | ||
+ | return os; | ||
+ | } | ||
+ | </ | ||
+ | ==重载的输出运算符应该最小化格式化操作== | ||
+ | 输出运算符通常不会对输出的内容进行格式化,尤其是**换行**的操作。该操作最好交由使用者来实现。 | ||
+ | ==重载的输入输出运算符必须是非成员函数== | ||
+ | 重载的输入输出运算符行**必须**是**非成员函数**。这是因为当重载运算符被定义为成员函数时,左边的算子必须是对应类的对象。如果不做调整,那么 '' | ||
+ | <code cpp> | ||
+ | Sales_data data; | ||
+ | data << cout; // if operator<< | ||
+ | </ | ||
+ | 若要将 | ||
+ | ===重载输入运算符 >>=== | ||
+ | 重载的输入运算符对参数以及返回值的要求是: | ||
+ | * 第一个参数为 '' | ||
+ | * 第二个参数为接收输入类对象的**non-const 引用**,因为输入会修改该对象的内容 | ||
+ | * 返回值为 '' | ||
+ | ==重载输入运算符需要检测输入是否合法== | ||
+ | 与重载的输出运算符最大的区别是,重载的输入运算符需要检测输入的结果是否合法。检测从以下几个方面入手: | ||
+ | * 检测的时机是当所有的输入完成以后 | ||
+ | * 检测的对象是输入对象 '' | ||
+ | * 非法输入的一般处理办法是直接赋予当前被输入对象一个默认值 | ||
+ | 以// | ||
+ | <code cpp> | ||
+ | istream & | ||
+ | { | ||
+ | double price; | ||
+ | is >> item.bookNo >> item.units_sold >> price; | ||
+ | if (is) // check that the inputs succeeded | ||
+ | item.revenue = item.units_sold * price; | ||
+ | else | ||
+ | item = Sales_data(); | ||
+ | return is; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 之所以要等到所有输入完成以后进行检测,是需要确保所有的输入值都是有效的;单独检查 '' | ||
+ | 除此之外,很多时候输入运算符还需要进行额外的检测。比如上例中的 '' | ||
+ | ====算术与关系运算符==== | ||
+ | 算术运算符与关系运算符的设计要点如下: | ||
+ | * 该类运算符应该被重载为**非成员函数**,因为需要允许左右算子的转换 | ||
+ | * 该类运算符的参数应该是 '' | ||
+ | * 该类运算符的返回值的方式是**值返回**,其返回值是运算结果**临时变量的拷贝** | ||
+ | * 如果算术运算符与赋值运算符都有定义,则复合算术运算符也应该被定义。 | ||
+ | 除此之外,使用复合运算符实现对应的算术运算符效率会更高:比如下例中,// | ||
+ | <code cpp> | ||
+ | // assumes that both objects refer to the same book | ||
+ | Sales_data | ||
+ | operator+(const Sales_data &lhs, const Sales_data &rhs) | ||
+ | { | ||
+ | Sales_data sum = lhs; // copy data members from lhs into sum | ||
+ | sum += rhs; // add rhs into sum | ||
+ | return sum; | ||
+ | } | ||
+ | </ | ||
+ | ===等价运算符=== | ||
+ | 重载等价运算符 '' | ||
+ | <code cpp> | ||
+ | bool operator==(const Sales_data &lhs, const Sales_data &rhs) | ||
+ | { | ||
+ | return lhs.isbn() == rhs.isbn() && | ||
+ | | ||
+ | | ||
+ | } | ||
+ | |||
+ | bool operator!=(const Sales_data &lhs, const Sales_data &rhs) | ||
+ | { | ||
+ | return !(lhs == rhs); | ||
+ | } | ||
+ | </ | ||
+ | 等价运算的重载需要遵循以下的原则: | ||
+ | * 如果类对象之间需要进行比较,那么 '' | ||
+ | * 等价应该被设计为有**传递性**,比如由 '' | ||
+ | * 如果类中重载了 '' | ||
+ | * '' | ||
+ | ===关系运算符=== | ||
+ | 关系运算符在某些需要进行排序的容器和算法中很有用,特别是 ''<'' | ||
+ | - 排序的关系应该遵循**严格弱序**(与关系容器的 key 要求一致) | ||
+ | - 排序的意义需要与等价运算符的意义**一致**,也就是说 '' | ||
+ | 第二点在评估是否需要定义关系运算符上尤为重要。以 // | ||
+ | - // | ||
+ | - 假设我们使用 '' | ||
+ | - 两个对象是**不相等**的(通过 '' | ||
+ | - 但两个对象又同时不小于对方(通过 ''<'' | ||
+ | 由上可以看出,''<'' | ||
+ | <WRAP center round important 100%> | ||
+ | 只有在**单一的排序逻辑**存在,并且该排序逻辑与 //==// 可以得出**一致结果**的时候,才可以进行关系运算符的重载。 | ||
+ | </ | ||
+ | ====赋值运算符(补充)==== | ||
+ | ==赋值运算符与初始化列表== | ||
+ | 除了之前提到过得赋值运算符的拷贝 / 移动形式,赋值运算符还允许将不同类型的数据赋予当前对象。一个重要的例子是 // | ||
+ | <code cpp> | ||
+ | vector< | ||
+ | v = {" | ||
+ | </ | ||
+ | 这种用法下的赋值运算符重载需要遵循以下要求: | ||
+ | * 返回值同样需要时当前对象的引用(与拷贝 / 移动赋值运算符一致) | ||
+ | * 不需要进行自我赋值的检测,因为参数类型与 '' | ||
+ | * **必须定义为类成员**,因为返回值要求。 | ||
+ | 下面是 //StrVec// 类中使用初始化列表赋值的赋值运算符实现(拷贝版本): | ||
+ | <code cpp> | ||
+ | StrVec & | ||
+ | { | ||
+ | // alloc_n_copy allocates space and copies elements from the given range | ||
+ | auto data = alloc_n_copy(il.begin(), | ||
+ | free(); | ||
+ | elements = data.first; // update data members to point to the new space | ||
+ | first_free = cap = data.second; | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | ==复合赋值运算符== | ||
+ | 书中推荐将复合赋值运算符与赋值运算符一起定义为**成员函数**,并返回**左算子的引用**。下面是 // | ||
+ | <code cpp> | ||
+ | // member binary operator: left-hand operand is bound to the implicit this pointer | ||
+ | // assumes that both objects refer to the same book | ||
+ | Sales_data& | ||
+ | { | ||
+ | units_sold += rhs.units_sold; | ||
+ | revenue += rhs.revenue; | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | ====下标运算符==== | ||
+ | 如果某个类是容器类,并且该容器支持通过位置获取元素,则我们应该为其重载下标运算符(// | ||
+ | * **必须是成员函数** | ||
+ | * 返回值是被获取元素的**引用**,这是为了与默认下标运算符保持一致 | ||
+ | * 针对 const / non-const 对象,下标运算符通常需要定义**两个版本**。由于返回引用,常量版本的返回值也需要加 const 确保不会被修改 | ||
+ | 下面是 //StrVec// 的下标操作符实现: | ||
+ | <code cpp> | ||
+ | class StrVec { | ||
+ | public: | ||
+ | std:: | ||
+ | { return elements[n]; | ||
+ | const std:: | ||
+ | { return elements[n]; | ||
+ | // other members as in § 13.5 (p. 526) | ||
+ | private: | ||
+ | std::string *elements; | ||
+ | }; | ||
+ | </ | ||
+ | 使用的时候需要确保下标访问的元素不为空(通常这部分验证可以集成到下标操作符的重载中)。除此之外,常量版版本的重载无法进行赋值: | ||
+ | <code cpp> | ||
+ | // assume svec is a StrVec | ||
+ | const StrVec cvec = svec; // copy elements from svec into cvec | ||
+ | // if svec has any elements, run the string empty function on the first one | ||
+ | if (svec.size() && svec[0].empty()) | ||
+ | svec[0] = " | ||
+ | cvec[0] = " | ||
+ | } | ||
+ | </ | ||
+ | ====自增自减运算符==== | ||
+ | 自增与自减运算符('' | ||
+ | 自增与自减的重载分为两个版本:前置版本 prefix 与 后置版本 postfix。两种版本有实现上的差异。 | ||
+ | ===前置自增/ | ||
+ | 前置自增运算符的重载需要满足几个条件: | ||
+ | * 返回值是当前对象的 non-const 引用 | ||
+ | * 参数列表为空 | ||
+ | 通常实现上,由于需要判断移位后的结果是否可以被解引用,因此需要**检查当前迭代器所在位置**。而根据移动方向的不同,自增自减的实现方法也不同。以 // | ||
+ | <code cpp> | ||
+ | // prefix: return a reference to the incremented/ | ||
+ | StrBlobPtr& | ||
+ | { | ||
+ | // if curr already points past the end of the container, can't increment it | ||
+ | check(curr, " | ||
+ | ++curr; | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | StrBlobPtr& | ||
+ | { | ||
+ | // if curr is zero, decrementing it will yield an invalid subscript | ||
+ | --curr; | ||
+ | check(curr, " | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 可以看到上面的实现中: | ||
+ | * 自增版本的实现是**先检测**,再自增 | ||
+ | * 自减版本的实现是**先自减**,再检测 | ||
+ | 这么做的原因是基于自增 / 自减的结果带来的影响: | ||
+ | * 如果是自增,那么需要验证的实际上是当前位置的合法性。如果当前位置已经超出序列的范围了,那么自增后的迭代器肯定是超出范围的。 | ||
+ | * 如果是自减,那么需要检测**自减后的位置**。比如 off-the-end 迭代器,该迭代器不指向任何有效内容,但其自检后则指向元素序列中最后一个元素。除此之外,// | ||
+ | ===后置自增/ | ||
+ | ==解决前置 / 后置版本定义中的二义性== | ||
+ | Built-in type 后置自增 / 自减运算符与前置版本最大的一个区别是,后置版本返回的是**没有自增之前的对象的状态**(以值方式返回);而除此之外并没有其他的区别。这带来一个问题,由于返回值的不同不计入重载的区别,因此前置版本与后置版本的参数数量和类型都是相同的;因此同时定义两个版本会带来二义性。\\ \\ | ||
+ | 为此,C++ 定义后置版本会**额外的接收一个** '' | ||
+ | ==StrBlobPtr 的后置版本实现== | ||
+ | 后置版本的实现可以基于前置版本: | ||
+ | <code cpp> | ||
+ | class StrBlobPtr { | ||
+ | public: | ||
+ | // increment and decrement | ||
+ | StrBlobPtr operator++(int); | ||
+ | StrBlobPtr operator--(int); | ||
+ | // other members as before | ||
+ | }; | ||
+ | |||
+ | // postfix: increment/ | ||
+ | StrBlobPtr StrBlobPtr:: | ||
+ | { | ||
+ | // no check needed here; the call to prefix increment will do the check | ||
+ | StrBlobPtr ret = *this; | ||
+ | ++*this; | ||
+ | return ret; // return the saved state | ||
+ | } | ||
+ | StrBlobPtr StrBlobPtr:: | ||
+ | { | ||
+ | // no check needed here; the call to prefix decrement will do the check | ||
+ | StrBlobPtr ret = *this; | ||
+ | --*this; | ||
+ | return ret; // return the saved state | ||
+ | } | ||
+ | </ | ||
+ | 可以看出来,后置版本的逻辑中,自增与返回值是分开的: | ||
+ | - 首先使用临时对象存储当前对象的状态 | ||
+ | - 通过使用 '' | ||
+ | - 返回之前存储的状态的拷贝 | ||
+ | 除此之外,参数中 '' | ||
+ | ===显式的调用自增自减函数=== | ||
+ | 自增 / 自减也可以函数的方式调用: | ||
+ | <code cpp> | ||
+ | StrBlobPtr p(a1); // p points to the vector inside a1 | ||
+ | p.operator++(0); | ||
+ | p.operator++(); | ||
+ | </ | ||
+ | 在这里,参数 '' | ||
+ | ====成员访问运算符==== | ||
+ | 成员访问运算符包括 '' | ||
+ | <code cpp> | ||
+ | class StrBlobPtr { | ||
+ | public: | ||
+ | std:: | ||
+ | { auto p = check(curr, " | ||
+ | return (*p)[curr]; | ||
+ | } | ||
+ | std:: | ||
+ | { // delegate the real work to the dereference operator | ||
+ | | ||
+ | } | ||
+ | // other members as before | ||
+ | }; | ||
+ | </ | ||
+ | 该实现的几个注意事项: | ||
+ | * 检查当前的位置是否有效,有效则返回当前 vector 的 shared_ptr '' | ||
+ | * 通过对 '' | ||
+ | * 箭头运算符的实现是直接使用当前的 // | ||
+ | 由于解引用和箭头运算都是**访问类型**的函数,因此这两个函数都是 const 成员函数。特别需要注意解引用重载返回了普通引用。这是因为我们可以确定 // | ||
+ | ===箭头运算符返回值的限制=== | ||
+ | 与其他的运算符重载不同,箭头运算符的重载**不能重新诠释**该运算符的意义。无论如何定义,该运算符始终意味着对**类成员的访问**。对箭头运算符的重载只能改变**被获取的成员**。\\ \\ | ||
+ | 当使用箭头运算符的时候,箭头的左边必须是一个**指向类对象的指针**.。比如 '' | ||
+ | <code cpp> | ||
+ | (*point).mem; | ||
+ | point.operator()-> | ||
+ | </ | ||
+ | * 第一种形式是解引用后使用对象调用成员 | ||
+ | * 第二种形式是使用对象直接调用 '' | ||
+ | 这两种不同的形式体现出重载箭头运算符拥有类似于递归的行为,即: | ||
+ | * 重载箭头运算符期望最终获取一个**build-in 指针**,并使用该指针调用 built-int type 的箭头运算符 | ||
+ | * 如果得到的结果是一个**指针类对象**,该类对象也重载了箭头运算符,则接着使用当前的类对象继续调用对应的箭头运算符。 | ||
+ | 也就是说,可以将**指针类对象视作中间过程**。每使用一次该对象重载的箭头运算符,就会解引用指针对象,并访问当前类对象中的指针成员。得到的指针成员可以用于调用下一个类对象中的指针成员,或是最终的实体成员;而调用的过程也是通过箭头运算符的重载来实现的: | ||
+ | \\ \\ {{ : | ||
+ | \\ | ||
+ | 这也解释了为了什么箭头运算符重载的返回值必须是**指针**的问题。 | ||
+ | ====函数调用运算符==== | ||
+ | 当一个类重载了函数调用运算符(// | ||
+ | 一个求绝对值的函数调用重载类的实现如下: | ||
+ | <code cpp> | ||
+ | struct absInt { | ||
+ | int operator()(int val) const { | ||
+ | return val < 0 ? -val : val; | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 该实现中,我们通过重载函数调用运算,实现了求绝对值的功能。通过调用该类的对象,可以像函数一样得到某个值的绝对值: | ||
+ | <code cpp> | ||
+ | nt i = -42; | ||
+ | absInt absObj; | ||
+ | int ui = absObj(i); // passes i to absObj.operator() | ||
+ | </ | ||
+ | 需要注意的是,函数调用的重载需要被**定义为成员函数**。该重载可以有多个版本。版本通过参数的数量与类型进行区分。由于函数调用的重载通过类对象实现了函数的功能,因此我们称该类对象为**函数对象**(// | ||
+ | ==函数对象类与状态== | ||
+ | 之前提到,函数对象类可以通过额外的数据成员来管理函数的状态。下面是一个打印带自定义分隔字符 string 的例子,其实现中使用了额外的数据成员存储自定义分隔字符: | ||
+ | <code cpp> | ||
+ | class PrintString { | ||
+ | public: | ||
+ | PrintString(ostream &o = cout, char c = ' '): | ||
+ | os(o), sep(c) { } | ||
+ | void operator()(const string &s) const { os << s << sep; } | ||
+ | private: | ||
+ | ostream & | ||
+ | char sep; // character to print after each output | ||
+ | }; | ||
+ | </ | ||
+ | 重载的函数调用运算符则负责输出获取的 string,以及自定义的分隔字符。调用的时候,只需要使用 // | ||
+ | <code cpp> | ||
+ | PrintString printer; | ||
+ | printer(s); | ||
+ | PrintString errors(cerr, | ||
+ | errors(s); | ||
+ | </ | ||
+ | 该类函数对象也可以用于泛型算法。比如将容器 '' | ||
+ | <code cpp> | ||
+ | for_each(vs.begin(), | ||
+ | </ | ||
+ | 需要注意这里的类对象是使用构造函数创建的**临时对象**。每个临时对象会对应每个元素,并将其打印出来。这里需要与一般的函数对象调用进行区别。 | ||
+ | ===Lambda 是函数对象=== | ||
+ | 函数对象的使用与 lambda 类似。实际上,lambda 在使用中被编译器转换为了一个无名类的无名对象。该对象也是通过函数调用运算符的重载来实现功能的。比如下面的 lambda 实际上与 // | ||
+ | <code cpp> | ||
+ | // sort words by size, but maintain alphabetical order for words of the same size | ||
+ | stable_sort(words.begin(), | ||
+ | [](const string &a, const string &b) | ||
+ | { return a.size() < b.size(); | ||
+ | | ||
+ | // | ||
+ | class ShorterString { | ||
+ | public: | ||
+ | bool operator()(const string &s1, const string &s2) const | ||
+ | { return s1.size() < s2.size(); } | ||
+ | }; | ||
+ | </ | ||
+ | 需要注意的是,lambda 不会改变捕获的变量。这一点体现到函数调用运算符中则意味着该运算符的重载应该是 **const 成员函数**('' | ||
+ | <code cpp> | ||
+ | stable_sort(words.begin(), | ||
+ | </ | ||
+ | ==如何用函数对象表示带捕获列表的 lambda== | ||
+ | lambda 分为引用捕获与值捕获。对应的函数对象的实现中,这两种捕获被进行了区分处理: | ||
+ | * 引用捕获:由于 lambda 本身不保证引用捕获列表中变量的有效性,因此在函数对象中不需要对引用类型的变量做存储。 | ||
+ | * 值捕获:值捕获列表中,原有变量会被拷贝并存储在 lambda 中。因此,这些变量也需要被存储到函数对象中。 | ||
+ | 一个值捕获 lambda 转换为函数对象的例子: | ||
+ | <code cpp> | ||
+ | // get an iterator to the first element whose size() is >= sz | ||
+ | auto wc = find_if(words.begin(), | ||
+ | [sz](const string &a) | ||
+ | </ | ||
+ | 其对应函数对象的实现为: | ||
+ | <code cpp> | ||
+ | class SizeComp { | ||
+ | SizeComp(size_t n): sz(n) { } // parameter for each captured variable | ||
+ | // call operator with the same return type, parameters, and body as the lambda | ||
+ | bool operator()(const string &s) const | ||
+ | { return s.size() >= sz; } | ||
+ | private: | ||
+ | size_t sz; // a data member for each variable captured by value | ||
+ | }; | ||
+ | </ | ||
+ | 这里,捕获列表中的变量以**私有数据成员**的形式存储到了函数对象 // | ||
+ | <code cpp> | ||
+ | // get an iterator to the first element whose size() is >= sz | ||
+ | auto wc = find_if(words.begin(), | ||
+ | </ | ||
+ | <color # | ||
+ | 这是因为使用 lambda 生成的函数类中,**默认构造函数是被删除的**(赋值运算符也是被删除的)。该类中包含一个默认的析构函数;其他的拷贝成员是否是被删除要**取决于被捕获的数据成员类型**。 | ||
+ | ===标准库函数对象=== | ||
+ | 标准库定义了一系列的函数对象,用于表示算术 / 关系 / 逻辑运算符。这些函数对象定义于 ''< | ||
+ | \\ \\ | ||
+ | {{ cs: | ||
+ | \\ \\ | ||
+ | 这些函数对象的特点是: | ||
+ | * 名字与运算符的意义相似 | ||
+ | * 实现上都是模板类,需要指定运算的类型 | ||
+ | * 可以嵌套 | ||
+ | 一些使用例子: | ||
+ | <code cpp> | ||
+ | plus< | ||
+ | negate< | ||
+ | // uses intAdd:: | ||
+ | int sum = intAdd(10, 20); // equivalent to sum = 30 | ||
+ | sum = intNegate(intAdd(10, | ||
+ | // uses intNegate:: | ||
+ | // to intAdd:: | ||
+ | sum = intAdd(10, intNegate(10)); | ||
+ | </ | ||
+ | ==在算法中使用标准库函数对象== | ||
+ | 标准库函数对象通常与算法搭配使用,用于复写算法中默认的运算符。比如 '' | ||
+ | <code cpp> | ||
+ | // passes a temporary function object that applies the < operator to two strings | ||
+ | sort(svec.begin(), | ||
+ | </ | ||
+ | 通过该复写,'' | ||
+ | ==标准库函数对象与指针== | ||
+ | 标准库函数对象有一个很重要的特性:这些对象**代表的运算可以正确的应用到指针上**。通常情况下,不同容器的指针进行运算的结果是未定义的;但通过使用标准库函数对象,我们仍然可以对这些指针进行运算。比如现在有一个 '' | ||
+ | <code cpp> | ||
+ | vector< | ||
+ | // ok: library guarantees that less on pointer types is well defined | ||
+ | sort(nameTable.begin(), | ||
+ | </ | ||
+ | 而如果使用自定义的 lambda,则无法进行排序: | ||
+ | <code cpp> | ||
+ | // error: the pointers in nameTable are unrelated, so < is undefined | ||
+ | sort(nameTable.begin(), | ||
+ | | ||
+ | </ | ||
+ | 实际上,关系容器中的排序就是通过标准库函数对象 '' | ||
+ | ===可调用对象与函数=== | ||
+ | C++ 中提供了如下几种可调用对象: | ||
+ | * 函数 | ||
+ | * 指向函数的指针 | ||
+ | * lambda | ||
+ | * bind 创建的对象 | ||
+ | * 函数对象 | ||
+ | 实现中,上述的可调用对象在具体的类型上可能有一些差异。但同时,这些可调用对象可能具有相同的 //Call Signature// | ||
+ | * 接收的参数数量与类型相同 | ||
+ | * 返回值的类型相同 | ||
+ | 比如下面的可调用对象,他们的 //Call Signature// 都是 '' | ||
+ | <code cpp> | ||
+ | // ordinary function | ||
+ | int add(int i, int j) { return i + j; } | ||
+ | // lambda, which generates an unnamed function-object class | ||
+ | auto mod = [](int i, int j) { return i % j; }; | ||
+ | // function-object class | ||
+ | struct divide { | ||
+ | int operator()(int denominator, | ||
+ | return denominator / divisor; | ||
+ | } | ||
+ | }; | ||
+ | </ | ||
+ | 假设我们希望将上述的各种可调用对象组合起来做一个 int 的计算器。一个比较好的方法是通过建立**函数表**(// | ||
+ | <code cpp> | ||
+ | // maps an operator to a pointer to a function taking two ints and returning an int | ||
+ | map< | ||
+ | </ | ||
+ | 上述的 map 中,key 的类型是 string,代表对应运算的符号;value 的类型是**函数名**(指针),代表了具体的可调用对象。当调用指定可调用对象时,只需要用下标访问对应的函数名既可。\\ \\ | ||
+ | ==标准库 function 类型== | ||
+ | 上面的例子中有一个问题:该 map 无法存储 lambda 类型的可调用对象: | ||
+ | <code cpp> | ||
+ | binops.insert({" | ||
+ | </ | ||
+ | 这是因为 '' | ||
+ | C++ 提供了 '' | ||
+ | \\ \\ < | ||
+ | 与具体类型不同,标准'' | ||
+ | <code cpp> | ||
+ | function< | ||
+ | </ | ||
+ | 也就是说,所有的,有两个 '' | ||
+ | <code cpp> | ||
+ | function< | ||
+ | function< | ||
+ | function< | ||
+ | { return i * j; }; | ||
+ | cout << f1(4,2) << endl; // prints 6 | ||
+ | cout << f2(4,2) << endl; // prints 2 | ||
+ | cout << f3(4,2) << endl; // prints 8 | ||
+ | </ | ||
+ | 因此,将 '' | ||
+ | <code cpp> | ||
+ | // table of callable objects corresponding to each binary operator | ||
+ | // all the callables must take two ints and return an int | ||
+ | // an element can be a function pointer, function object, or lambda | ||
+ | map< | ||
+ | {" | ||
+ | {" | ||
+ | {"/", | ||
+ | {" | ||
+ | {" | ||
+ | </ | ||
+ | 由于 '' | ||
+ | <code cpp> | ||
+ | binops[" | ||
+ | binops[" | ||
+ | binops["/" | ||
+ | binops[" | ||
+ | binops[" | ||
+ | </ | ||
+ | ==重载函数与 function 类型== | ||
+ | 当程序中存在函数的重载时,**不能将函数名**作为 '' | ||
+ | <code cpp> | ||
+ | int add(int i, int j) { return i + j; } | ||
+ | Sales_data add(const Sales_data&, | ||
+ | map< | ||
+ | binops.insert( {" | ||
+ | </ | ||
+ | 这种情况下有两种办法: | ||
+ | * 重命名函数的指针作为 '' | ||
+ | * 使用 lambda 作为 '' | ||
+ | <code cpp> | ||
+ | int (*fp)(int, | ||
+ | binops.insert( {" | ||
+ | // ok: use a lambda to disambiguate which version of add we want to use | ||
+ | binops.insert( {" | ||
+ | </ | ||
+ | ====重载,类型转换,运算符==== | ||
+ | C++ 允许用户通过**重载类型转换运算符**的方式自定义 class-type 与 其他类型的转换。这类转换被称为**类类型转换**(// | ||
+ | ===类型转换运算符=== | ||
+ | 类型转换运算符(// | ||
+ | * **不能**定义到 '' | ||
+ | * 没有显式的返回值(也不能写),返回值类型**不能是** '' | ||
+ | * 没有显式的参数列表(也不能写),因为类型转换运算符是隐式的应用的 | ||
+ | * 必须是**成员函数** | ||
+ | * 应该是 const 成员,因为类型转换不会改变被转换的对象 | ||
+ | 其定义写法如下: | ||
+ | <code cpp> | ||
+ | operator type() const; | ||
+ | </ | ||
+ | 其中 '' | ||
+ | ==定义类型转换运算符== | ||
+ | 一个简单的类类型与 '' | ||
+ | <code cpp> | ||
+ | class SmallInt { | ||
+ | public: | ||
+ | SmallInt(int i = 0): val(i) | ||
+ | { | ||
+ | if (i < 0 || i > 255) | ||
+ | throw std:: | ||
+ | } | ||
+ | operator int() const { return val; } | ||
+ | private: | ||
+ | std::size_t val; | ||
+ | }; | ||
+ | </ | ||
+ | 在该定义中,任意的算术类型可以通过构造函数转换为 '' | ||
+ | <code cpp> | ||
+ | SmallInt si; | ||
+ | si = 4; // implicitly converts 4 to SmallInt then calls SmallInt:: | ||
+ | si + 3; // implicitly converts si to int followed by integer addition | ||
+ | </ | ||
+ | 除此之外用户自定义的类型转换结果用于计算时也可能会发生隐式转换: | ||
+ | <code cpp> | ||
+ | // the double argument is converted to int using the built-in conversion | ||
+ | SmallInt si = 3.14; // calls the SmallInt(int) constructor | ||
+ | // the SmallInt conversion operator converts si to int; | ||
+ | si + 3.14; // that int is converted to double using the built-in conversion | ||
+ | </ | ||
+ | 上述例子中,'' | ||
+ | ==避免滥用自定义类型转换== | ||
+ | 重载类型转换运算符有一个前提:// | ||
+ | \\ \\ | ||
+ | 也就是说,转换只能在没有歧义的对应关系中发生。比如类类型 '' | ||
+ | \\ \\ | ||
+ | 当我们可以找出不止一种,但又是合理的表达方式的时候,类型转换运算符是**不应该**被重载的。这种情况下,我们应该**定义不同的成员函数**来提取不同的信息。 | ||
+ | ==explicit 类型转换运算符== | ||
+ | 在类中定义类类型到 '' | ||
+ | <code cpp> | ||
+ | int i = 42; | ||
+ | cin << i; // this code would be legal if the conversion to bool were not explicit! | ||
+ | </ | ||
+ | 假设上面的例子中, | ||
+ | - 首先,'' | ||
+ | - 由于 '' | ||
+ | - 最后,'' | ||
+ | 为了避免这样的情况发生,C++11 提供了 '' | ||
+ | <code cpp> | ||
+ | class SmallInt { | ||
+ | public: | ||
+ | // the compiler won't automatically apply this conversion | ||
+ | explicit operator int() const { return val; } | ||
+ | // other members as before | ||
+ | }; | ||
+ | </ | ||
+ | 重载的类型转换运算符被 '' | ||
+ | <code cpp> | ||
+ | SmallInt si = 3; // ok: the SmallInt constructor is not explicit | ||
+ | si + 3; // error: implicit is conversion required, but operator int is explicit | ||
+ | static_cast< | ||
+ | </ | ||
+ | 需要注意的是,上述的限制有一个非常重要的**例外**。当 '' | ||
+ | * 用于 '' | ||
+ | * 用于 '' | ||
+ | * 用于逻辑运算 '' | ||
+ | * 用于三目运算符 ''?:'' | ||
+ | ==IO类型中,基于 C++11 的 bool 转换== | ||
+ | 早期的 C++ 中,IO 类型通过定义到 '' | ||
+ | <code cpp> | ||
+ | while (std::cin >> value) | ||
+ | </ | ||
+ | 此处的 '' | ||
+ | <WRAP center round tip 100%> | ||
+ | 定义类类型到 bool 类型的转换通常是为了使用类类型作为判断条件。因此最好将该重载定义为 // | ||
+ | </ | ||
+ | ===避免转换中的二义性=== | ||
+ | 如果类中存在多个类型转换时,需要确保 class-type 与目标类型之间的转换是**唯一**的。如果对同一个类型的转换存在多种方式,很可能会导致潜在的二义性错误。有两个比较典型的例子: | ||
+ | * class-type 之间存在**交互转换**。具体来说,B 通过 A 的构造函数转换为 A,而 B 中又重载了到 A 的转换运算 | ||
+ | * 类中定义了多种将 class-type 转换为目标类型(绝大部分是算术类型)的方式 | ||
+ | ==交互转换导致二义性的实例== | ||
+ | 下面的代码是一个交互转换导致二义性的例子: | ||
+ | <code cpp> | ||
+ | // usually a bad idea to have mutual conversions between two class types | ||
+ | struct B; | ||
+ | struct A { | ||
+ | A() = default; | ||
+ | A(const B& | ||
+ | // other members | ||
+ | }; | ||
+ | struct B { | ||
+ | operator A() const; // also converts a B to an A | ||
+ | // other members | ||
+ | }; | ||
+ | A f(const A&); | ||
+ | B b; | ||
+ | A a = f(b); // error ambiguous: f(B:: | ||
+ | // or f(A:: | ||
+ | </ | ||
+ | 该段代码中: | ||
+ | * A 可以通过构造函数将 B 转换为 A 类型 | ||
+ | * B 中定义了 B 到 A 的转换,因此可以通过隐式转换将 B 转换为 A | ||
+ | 当调用 '' | ||
+ | <code cpp> | ||
+ | A a1 = f(b.operator A()); // ok: use B's conversion operator | ||
+ | A a2 = f(A(b)); | ||
+ | </ | ||
+ | ==转换方式不唯一导致二义性的实例== | ||
+ | 转换方式不唯一导致二义性通常发生在**类类型到算术类型**的转换过程中。比如下面的例子: | ||
+ | <code cpp> | ||
+ | struct A { | ||
+ | A(int = 0); // usually a bad idea to have two | ||
+ | A(double); | ||
+ | operator int() const; | ||
+ | operator double() const; // conversions to arithmetic types | ||
+ | // other members | ||
+ | |||
+ | }; | ||
+ | void f2(long double); | ||
+ | A a; | ||
+ | f2(a); // error ambiguous: f(A:: | ||
+ | // | ||
+ | long lg; | ||
+ | A a2(lg); // error ambiguous: A::A(int) or A:: | ||
+ | </ | ||
+ | 上例中,类 A 中定义了: | ||
+ | * 两种构造方式,将**不同的算术类型**转换为 A 类型 | ||
+ | * 两种转换方式,将 A 转换为**不同类型**的算术类型 | ||
+ | 这样的设计会导致以下的调用出现二义性: | ||
+ | * 当调用者使用 A 类型的参数时,A 类型需要首先转换为适合 '' | ||
+ | * 同理,当调用者使用 '' | ||
+ | 可以发现的是,在上述的类型转换过程中,编译器是通过**最佳匹配**的方式来选择合适的转换方式的。由于上述的转换都是处于算术类型转换的等级,因此编译器无法识别哪种方式更匹配。只要转换方式中存在不同的转换等级,那么二义性就不会发生。比如: | ||
+ | <code cpp> | ||
+ | short s = 42; | ||
+ | // promoting short to int is better than converting short to double | ||
+ | A a3(s); | ||
+ | </ | ||
+ | 这里的参数类型是 '' | ||
+ | <WRAP center round info 100%> | ||
+ | 当存在多个用户自定义的转换时,如果转换之前 / 之后存在标准类型的转换,则使用**最佳匹配**的策略来决定使用哪个转换。 | ||
+ | </ | ||
+ | <WRAP center round box 100%> | ||
+ | 正确设计自定义类型转换的几个准则: | ||
+ | - 不要定义类之间的交互转换。 | ||
+ | - 避免到算术类型的类型转换。特别是如果已经定义了一个到算术类型的转换: | ||
+ | - 不要定义接收其他算术类型的构造函数 | ||
+ | - 不要重载到别的算术类型的转换运算符 | ||
+ | - 使用已经定义的转换,以及算术类型自身的标准转换来得到最终的结果 | ||
+ | 总而言之,除了 '' | ||
+ | </ | ||
+ | |||
+ | ==重载函数与转换构造函数== | ||
+ | 除了以上的两种情况以外,如果**不同的类中同时定义了到某个目标类型的转换**,那么这些转换会被视为具有同等的优先级。比如下面的例子: | ||
+ | <code cpp> | ||
+ | struct C { | ||
+ | C(int); | ||
+ | // other members | ||
+ | }; | ||
+ | struct D { | ||
+ | D(int); | ||
+ | // other members | ||
+ | }; | ||
+ | void manip(const C&); | ||
+ | void manip(const D&); | ||
+ | manip(10); // error ambiguous: manip(C(10)) or manip(D(10)) | ||
+ | </ | ||
+ | 在这个例子中,由于 C 和 D 都定义了转换到 '' | ||
+ | <code cpp> | ||
+ | manip(C(10)); | ||
+ | </ | ||
+ | <WRAP center round important 100%> | ||
+ | 如果存在需要对参数进行类型转换的构造函数,那么说明程序的设计存在不足。 | ||
+ | </ | ||
+ | ==重载函数与用户自定义的类型转换== | ||
+ | 需要注意的是,当同时存在两个或以上的最佳自定义类型转换方案时,整个转换过程中可能会出现的**标准转换**将**不再作为选择转换方案的参考依据**。具体来讲,只有**当使用同一个自定义转换方案的时候,标准转换会被纳入考虑的范围**(参考第二种二义性的情况)。比如下面的例子: | ||
+ | <code cpp> | ||
+ | struct E { | ||
+ | E(double); | ||
+ | // other members | ||
+ | }; | ||
+ | void manip2(const C&); | ||
+ | void manip2(const E&); | ||
+ | // error ambiguous: two different user-defined conversions could be used | ||
+ | manip2(10); // manip2(C(10) or manip2(E(double(10))) | ||
+ | [</ | ||
+ | 这种情况下,虽然 E 默认使用 '' | ||
+ | ===函数匹配与运算符重载=== | ||
+ | 重载的运算符实际上是重载的函数。但与一般的重载函数的匹配规则不同,当重载运算符用于表达式中,比起一般使用 '' | ||
+ | 需要注意的是,即便运算符重载可以写成如下的函数形式: | ||
+ | <code cpp> | ||
+ | a.operatorsym (b); // a has operatorsym as a member function | ||
+ | operatorsym(a, | ||
+ | </ | ||
+ | 编译器也**不能根据该形式**的不同决定调用成员或者非成员函数。 | ||
+ | ==使用类对象作为重载运算符的算子== | ||
+ | 当某个表达式中,如果参与运算的算子包含 class-type,且该 class-type 中重载了此运算,则: | ||
+ | * 候选的重载函数包括: | ||
+ | * 非成员函数版本的运算符 | ||
+ | * bulit-in 版本的运算符 | ||
+ | * 如果**左算子**是 class-type 的对象,候选函数还应该包括其自身的**成员运算符**版本 | ||
+ | 在使用的过程中: | ||
+ | * 如果用 // | ||
+ | * 如果以**表达式的形式**使用重载运算符,则会将运算符的**成员**重载与**非成员**重载**同时纳入候选**。 | ||
+ | 来看下面的例子: | ||
+ | <code cpp> | ||
+ | class SmallInt { | ||
+ | friend | ||
+ | SmallInt operator+(const SmallInt&, | ||
+ | public: | ||
+ | SmallInt(int = 0); // conversion from int | ||
+ | operator int() const { return val; } // conversion to int | ||
+ | private: | ||
+ | std::size_t val; | ||
+ | }; | ||
+ | </ | ||
+ | 我们对其进行如下的调用: | ||
+ | <code cpp> | ||
+ | SmallInt s1, s2; | ||
+ | SmallInt s3 = s1 + s2; // uses overloaded operator+ | ||
+ | int i = s3 + 0; // error: ambiguous | ||
+ | </ | ||
+ | '' | ||
+ | <code cpp> | ||
+ | SmallInt operator+(const SmallInt&, | ||
+ | </ | ||
+ | 而 '' | ||
+ | * 由于运算 '' | ||
+ | * 由于 '' | ||
+ | * 由于 | ||
+ | <code cpp> | ||
+ | // | ||
+ | SmallInt operator+(const SmallInt&, | ||
+ | //// | ||
+ | int operator(int, | ||
+ | </ | ||
+ | * 当前情况下,我们可以通过转换 '' | ||
+ | <WRAP center round important 100%> | ||
+ | 如果类中同时存在 **class-type 到算术类型的转换**与**运算符的重载**,那么很可能会导致重载运算符与 built-in 运算符之间的二义性。 | ||
+ | </ | ||