Part III: Tools for Class Authors
Chapter 14. Overloaded Operations and Conversions
14.8 函数调用运算符
如果类重载调用运算符,那么可以像使用函数那样使用该类的对象。因为这样的类还可以存储状态,所以它们比普通函数更灵活。
struct absInt {
int operator()(int val) const {
return val < 0 ? -val : val; // 接受一个 int 值并返回其绝对值
}
};
int i = -42;
absInt absObj; // object that has a function-call operator
int ui = absObj(i); // passes i to absObj.operator()
注:函数调用运算符必须是成员函数。类可以定义多个版本的函数调用运算符,每个版本的形参数量或类型必须有所区别。
如果类定义了调用运算符,那么该类的对象被称为函数对象 (function object)。这样的对象“行为像函数一样”,因为可以调用它们。
具有状态的函数对象类
函数对象类通常含有一些数据成员,这些成员被用于定制调用运算符中的操作。
// define a class that prints a string argument
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; // stream on which to write
char sep; // character to print after each output
};
PrintString printer; // uses the defaults; prints to cout
printer(s); // prints s followed by a space on cout
PrintString errors(cerr, '\n');
errors(s); // prints s followed by a newline on cerr
函数对象最常用作泛型算法的实参。例如,可以使用库 for_each 算法和 PrintString 类来打印容器的内容:
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
lambda 是函数对象
当编写 lambda 时,编译器将该表达式转换为未命名类的未命名对象。从 lambda 生成的类包含一个重载的函数调用运算符。
// sort words by size, but maintain alphabetical order for words of the same size
stable_sort(words.begin(), words.end(), [](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 表达式不能更改其捕获的变量。因此,默认情况下,从 lambda 生成的类中的函数调用运算符是 const 成员函数。如果 lambda 被声明为可变的,则调用运算符不是 const。
stable_sort(words.begin(), words.end(), ShorterString());
表示具有捕获行为的 lambda 表达式的类
当 lambda 通过引用捕获变量时,程序要确保在执行 lambda 时所引用的变量存在。因此,允许编译器直接使用引用,而无需将该引用作为数据成员存储在生成的类中。
相反,通过值捕获的变量被复制到 lambda 中。因此,按值捕获变量的 lambda 生成的类具有对应于每个这种变量的数据成员。这些类还具有一个构造函数,使用捕获变量的值来初始化这些数据成员。
// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(), [sz](const string &a)
上面 lambda 生成的类形似:
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
};
这个合成的类没有默认构造函数,要使用这个类,必须传递一个实参。
// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));
lambda 表达式生成的类具有删除的默认构造函数、删除的赋值运算符、默认析构函数。类是否具有默认的或删除的复制/移动构造函数,通常情况下取决于捕获的数据成员的类型。
标准库定义的函数对象
标准库定义了一组表示算术、关系和逻辑运算符的类。每个类都定义一个调用运算符,应用于命名操作。例如,plus 类具有一个函数调用运算符,该运算符将 + 应用于一对运算对象;modulus 类定义了应用于二元 % 运算符的调用运算符;equal_to 类应用于 ==;等等。
这些类是我们提供单一类型的模板。该类型指定调用运算符的形参类型。例如,plus<string> 将 string 加法运算符应用于 string 对象;plus<Sales_data> 应用于 Sales_datas;等等。
plus<int> intAdd; // function object that can add two int values
negate<int> intNegate; // function object that can negate an int value
// uses intAdd::operator(int, int) to add 10 and 20
int sum = intAdd(10, 20); // equivalent to sum = 30
sum = intNegate(intAdd(10, 20)); // equivalent to sum = 30
// uses intNegate::operator(int) to generate -10 as the second parameter
// to intAdd::operator(int, int)
sum = intAdd(10, intNegate(10)); // sum = 0
这些类型定义在 functional 头文件中。
表14.2 标准库函数对象
算术 | 关系 | 逻辑 |
---|---|---|
plus<Type> | equal_to<Type> | logical_and<Type> |
minus<Type> | not_equal_to<Type> | logical_or<Type> |
multiplies<Type> | greater<Type> | logical_not<Type> |
divide<Type> | greater_equal<Type> | |
modulus<Type> | less<Type> | |
negate<Type> | less_equal<Type> |
在算法中使用标准库函数对象
表示运算符的函数对象类通常用于覆盖算法使用的默认运算符。默认情况下,排序算法使用 operator<,一般将序列按升序排序。要按降序排序,可以传递一个 greater 类型的对象。该类生成一个调用运算符,该运算符调用基础元素类型的大于运算符。
// passes a temporary function object that applies the < operator to two strings
sort(svec.begin(), svec.end(), greater<string>());
标准库保证库函数对象适用于指针。
vector<string *> nameTable; // vector of pointers
// error: the pointers in nameTable are unrelated, so < is undefined
sort(nameTable.begin(), nameTable.end(), [](string *a, string *b) { return a < b; });
// ok: library guarantees that less on pointer types is well defined
sort(nameTable.begin(), nameTable.end(), less<string*>());
注意,关联容器使用 less<key_type> 对其元素进行排序。因此,可以定义一个指针 set 或将指针用作 map 中的键,而无需直接指定 less。
可调用对象与 function
C++有几种可调用对象:函数、函数指针、lambda 表达式、bind 创建的对象、重载了函数调用运算符的类。
与其他对象类似,可调用对象也有类型。例如,lambda 有它自己唯一的(未命名)类类型。函数与函数指针的类型因为它们的返回类型和实参类型而异,等等。
然而,两个不同类型的可调用对象可以共享相同的调用特征标 (call signature)。调用特征标指定了对象调用返回的类型以及必须在调用中传递的实参类型。一个调用特征标对应于一个函数类型。例如:
int(int, int)
是一个函数类型,它接受两个 int,返回一个 int。
不同的类型可以具有相同的调用特征标
对于共享一个调用特征标的几个可调用对象,有时我们希望将它们视为具有相同类型。例如,考虑以下不同类型的可调用对象:
// 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 div {
int operator()(int denominator, int divisor) {
return denominator / divisor;
}
};
这些可调用对象中的每一个都对其形参应用算术运算。即使每个都有不同的类型,但它们都共享相同的调用特征标:
int(int, int)
我们可能想使用这些可调用对象来构建一个简单的桌面计算器。为此,我们想定义一个函数表 (function table) 来存储指向这些可调用对象的“指针”。当程序需要执行特定操作时,它将在表中查找要调用的函数。
在C++中,使用 map 很容易实现函数表。 在这种情况下,将与运算符符号对应的 string 作为键,将实现该运算符的函数作为值。当为给定的运算符求值时,通过该运算符索引 map 并调用结果元素。
如果我们所有的函数都是独立函数,并且假设仅处理 int 类型的二元运算符,则可以将 map 定义为
// maps an operator to a pointer to a function taking two ints and returning an int
map<string, int(*)(int,int)> binops;
可以将 add 的指针放入 binops 中:
// ok: add is a pointer to function of the appropriate type
binops.insert({"+", add}); // {"+", add} is a pair § 11.2
但不能在 binops 中存储 mod 或 div:
binops.insert({"%", mod}); // error: mod is not a pointer to function
问题在于 mod 是一个lambda 表达式,每个 lambda 表达式都有自己的类类型。该类型与 binop 中存储的值的类型不匹配。
标准库 function 类型
可以使用C++11标准库类型 function
来解决上面的问题,function 定义在 functional 头文件中。
表14.3 function 定义的操作
操作 | 说明 |
---|---|
function<T> f; | f 是一个可以存储可调用对象的空 function 对象,其可调用对象的调用特征标等于函数类型 T,即 T 是 retType(args)。 |
function<T> f(nullptr); | 显式地构造一个空 function。 |
function<T> f(obj); | 在 f 中存储一个可调用对象 obj 的副本。 |
f | 使用 f 作为条件;若 f 存储一个可调用对象则为 true,否则 false。 |
f(args) | 调用 f 中的对象,传递参数 args。 |
定义为 function<T> 成员的类型
类型 | 说明 |
---|---|
result_type | 这个 function 类型的可调用对象返回的类型。 |
argument_type | 当 T 有一个或两个实参时定义的类型。如果 T 有一个实参,argument_type 是那个类型的同义词。 |
first_argument_type | 如果 T 有两个实参,first_argument_type 和 second_argument_type 是那些实参类型的同义词。 |
second_argument_type | 见上 |
function
是一个模板,当创建一个 function 类型时,必须指定额外的信息。这个信息是指这个特定 function 类型可以表示的对象的调用特征标。在尖括号内指定类型:
function<int(int, int)>
这声明一个 function 类型,可以表示一个返回 int 结果且具有两个 int 形参的可调用对象。
可以使用这个类型表示任意一种桌面计算器类型:
function<int(int, int)> f1 = add; // function pointer
function<int(int, int)> f2 = div(); // object of a function-object class
function<int(int, int)> f3 = [](int i, int j) // lambda
{ return i * j; };
cout << f1(4,2) << endl; // prints 6
cout << f2(4,2) << endl; // prints 2
cout << f3(4,2) << endl; // prints 8
// 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<string, function<int(int, int)>> binops;
map<string, function<int(int, int)>> binops = {
{"+", add}, // function pointer
{"-", std::minus<int>()}, // library function object
{"/", div()}, // user-defined function object
{"*", [](int i, int j) { return i * j; }}, // unnamed lambda
{"%", mod} }; // named lambda object
binops["+"](10, 5); // calls add(10, 5)
binops["-"](10, 5); // uses the call operator of the minus<int> object
binops["/"](10, 5); // uses the call operator of the div object
binops["*"](10, 5); // calls the lambda function object
binops["%"](10, 5); // calls the lambda function object
重载的函数与 function
不能(直接)在类型 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} ); // error: which add?
解决这个二义性问题的一种方式是,存储函数指针,替代函数名。
int (*fp)(int,int) = add; // pointer to the version of add that takes two ints
binops.insert( {"+", fp} ); // ok: fp points to the right version of add
还可以使用 lambda 消除二义性。
// ok: use a lambda to disambiguate which version of add we want to use
binops.insert( {"+", [](int a, int b) {return add(a, b);} } );
注:C++11标准库中的 function 类与旧版本库中的 unary_function 和 binary_function 类没有关联。这些类已被更通用的 bind 函数弃用。
14.9 重载、类型转换与运算符
可以由一个实参调用的非 explicit 构造函数,定义一种隐式转换。这样的构造函数将对象从实参的类型转换为类类型。
还可以定义类型转换,将类类型转换为其他类型。可以通过定义转换运算符来定义这样的转换。
转换构造函数和转换运算符定义类类型转换 (class-type conversions)。这种转换也称为用户定义的类型转换 (user-defined conversions)。
类型转换运算符
类型转换运算符 (conversion operator) 是一种特殊的成员函数,它将一个类类型的值转换为其他类型的值。
类型转换函数一般形式如下:
operator type() const;
其中 type 表示某种类型。如果某个类型可以作为函数的返回类型(void 除外),那么类型转换运算符就可以为其定义。
不允许转换为数值或函数类型。
允许转换为指针类型(数据和函数指针都可以)和引用类型。
类型转换运算符没有显式声明的返回类型,也没有形参,它们必须被定义为成员函数。类型转换操作一般不应更改其转换的对象。因此,类型转换运算符通常被定义为 const 成员。
定义具有类型转换运算符的类
class SmallInt {
public:
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 类定义了从/向类类型的转换。
构造函数将算术类型的值转换为 SmallInt 对象,类型转换运算符将 SmallInt 对象转换为 int。
SmallInt si;
si = 4; // implicitly converts 4 to SmallInt then calls SmallInt::operator=
si + 3; // implicitly converts si to int followed by integer addition
尽管编译器一次只使用一次用户定义的类型转换,但是隐式用户定义的类型转换可以在标准(内置)类型转换之前或之后。因此,可以将任何算术类型传递给 SmallInt 构造函数。类似地,可以使用类型转换运算符将 SmallInt 转换为 int,然后将所得的 int 值转换为另一种算术类型。
// 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
由于类型转换运算符是隐式执行的,因此无法将实参传递给这些函数。因此,定义类型转换运算符时无法接受形参。尽管类型转换函数未指定返回类型,但是每个转换函数必须返回其对应类型的值。
class SmallInt;
operator int(SmallInt&); // error: nonmember
class SmallInt {
public:
int operator int() const; // error: return type
operator int(int = 0) const; // error: parameter list
operator int*() const { return 42; } // error: 42 is not a pointer
};
注意:避免过度使用转换函数。
类型转换运算符可能产生意外结果
在实践中,类很少提供类型转换运算符。如果类型转换是自动发生的,那么用户通常会为这个转换感到惊讶,而不是感觉受到了帮助。但是,这个经验法则有一个重要的例外:对于类来说,定义向 bool 的转换并不少见。
在C++标准的早期版本中,想要定义向 bool 的转换的类面临一个问题:由于 bool 是算术类型,如果类类型的对象可以转换为 bool,那么可以在任何需要算术类型的上下文中使用它。
特别是,如果 istream 具有向 bool 的转换时,那么会编译以下代码:
int i = 42;
cin << i; // this code would be legal if the conversion to bool were not explicit!
// the promoted bool value (either 1 or 0) would be shifted left 42 positions.
istream 没有定义 <<,因此代码应该是错误的。但是,此代码可以使用 bool 转换运算符将 cin 转换为 bool。然后,将 bool 值提升为 int,并用作左移运算符的内置版本的左侧运算对象。
explicit 类型转换运算符
为了防止这样的问题,C++11标准引入了 explicit 类型转换运算符 (explicit conversion operator)。
class SmallInt {
public:
// the compiler won't automatically apply this conversion
explicit operator int() const { return val; }
// other members as before
};
SmallInt si = 3; // ok: the SmallInt constructor is not explicit
si + 3; // error: implicit is conversion required, but operator int is
explicit static_cast<int>(si) + 3; // ok: explicitly request the conversion
如果类型转换运算符是 explicit,仍然可以进行转换。但是必须通过显式强制转换这样做,除了一个例外。
这唯一的例外是编译器将 explicit 类型转换应用于用作条件的表达式。即,当表达式是用于下面这些情况时,explicit 类型转换将隐式地用于转换表达式:
- if、while 或 do 语句的条件
- for 语句头的条件表达式
- 逻辑非
!
、或||
、与&&
运算符的一个运算对象 - 条件运算符
? :
的条件表达式
转换为 bool
转换为 bool 通常意图在条件中使用。因此,operator bool 一般应定义成 explicit。
避免二义性的类型转换
如果类有一个或多个类型转换,确保从类类型转换为目标类型只有一种方式。
在两种情况下可能发生多重转换路径:
- 两个类提供相互转换。例如,当类A定义接受类B的对象的转换构造函数,且B本身定义转换为类型A的类型转换运算符时,则存在相互转换。
- 定义多个向/从某些类型的转换,这些转换本身可以通过类型转换相关联。最明显的例子是内置的算术类型。一个给定的类通常应该最多定义一个向/从算术类型的转换。
实参匹配与相互类型转换
// usually a bad idea to have mutual conversions between two class types
struct B;
struct A {
A() = default;
A(const B&); // converts a B to an A
// 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::operator A())
// or f(A::A(const B&))
如果想要要进行此调用,则必须显式调用类型转换运算符或构造函数:
A a1 = f(b.operator A()); // ok: use B's conversion operator
A a2 = f(A(b)); // ok: use A's constructor
注意:不能使用强制类型转换解决二义性问题,因为强制类型转换本身也具有相同的二义性。
二义性与转换为内置类型的多重转换
struct A {
A(int = 0); // usually a bad idea to have two
A(double); // conversions from arithmetic types
operator int() const; // usually a bad idea to have two
operator double() const; // conversions to arithmetic types
// other members
};
void f2(long double);
A a;
f2(a); // error ambiguous: f(A::operator int())
// or f(A::operator double())
long lg;
A a2(lg); // error ambiguous: A::A(int) or A::A(double)
对 f2 的调用以及对 a2 的初始化是二义性的,因为所需的标准类型转换级别相同。当使用用户定义的转化时,根据标准类型转换的级别(如果有)选择最佳匹配:
short s = 42;
// promoting short to int is better than converting short to double
A a3(s); // uses A::A(int)
重载函数与转换构造函数
当调用重载函数时,在多个类型转换之间进行选择会更加复杂。如果两次或多次的类型转换提供了可行的匹配,则认为这些转换同样有效。
例如,如果重载函数接受的形参是不同的类类型,但这些类定义了相同的转换构造函数,那么可能会产生二义性问题:
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))
调用者可以通过显式构造正确的类型来消除二义性:
manip(C(10)); // ok: calls manip(const C&)
警告:如果在调用重载函数时,需要频繁使用构造函数或强制转换来转换实参的类型,这表示不是一个好的设计。
重载函数与用户定义的类型转换
在对重载函数的调用中,如果两个(或多个)用户定义的类型转换提供了可行的匹配,则认为这些转换同样好。不考虑可能需要或不需要的任何标准转换的级别。仅当重载集合可以使用相同的转换函数进行匹配时,才考虑是否还需要内置转换。
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)))
函数匹配与重载运算符
重载运算符是重载函数。正常函数匹配可以用来确定给定表达式使用的是哪种运算符,是内置的还是重载的。但是,当在表达式中使用运算符函数时,候选函数集比使用调用运算符调用函数时要大。如果 a 是类类型,则表达式 a sym b 可能是
a.operator<sym>(b); // a has operatorsym as a member function
operator<sym>(a, b); // operatorsym is an ordinary function
无法使用调用形式来区分调用的是非成员函数还是成员函数。
表达式中使用的运算符的候选函数集可以包含非成员函数和成员函数。
class SmallInt {
friend
SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int = 0); // conversion from int
operator int() const { return val; } // conversion to int
private:
std::size_t val;
};
SmallInt s1, s2;
SmallInt s3 = s1 + s2; // uses overloaded operator+
int i = s3 + 0; // error: ambiguous
如果同时提供转换到算术类型的函数与相同类型的重载运算符,可能会导致重载运算符和内置运算符之间的二义性问题。