《C++ Primer第五版》读书笔记(12)-Overloaded Operations and Conversions
本章没啥新意,都是老知识了.
15.1 Basic Concepts
Overloaded operators are functions with special names: the keyword operator followed by the symbol for the operator being defined. Like any other function, an overloaded operator has a return type, a parameter list, and a body.An overloaded operator function has the same number of parameters as the operator has operands. A unary operator has one parameter; a binary operator has two. In a binary operator, the left-hand operand is passed to the first parameter and the right-hand operand to the second. Except for the overloaded function-call operator, operator(), an overloaded operator may not have default arguments.
If an operator function is a member function, the first (left-hand) operand is bound to the implicit this pointe. Because the first operand is implicitly bound to this, a member operator function has one less (explicit) parameter than the operator has operands.
15.2 Input and Output Operators
15.2.1 Overloading the Output Operator <<
Ordinarily, the first parameter of an output operator is a reference to a nonconst ostream object. The ostreamis nonconstbecause writing to the stream changes its state. The parameter is a reference because we cannot copy an ostream object.
The second parameter ordinarily should be a reference to constof the class type we want to print. The parameter is a reference to avoid copying the argument. It can be const because (ordinarily) printing an object does not change that object.
To be consistent with other output operators, operator<<normally returns its ostream parameter.
ostream &operator<<(ostream &os, const Sales_data &item)
{
os << item.isbn() << " " << item.units_sold << " "<< item.revenue << " " <<item.avg_price();
return os;
}
Generally,output operators should print the contents of the object, with minimal formatting. They should not print a newline. An output operator that does minimal formatting lets users control the details of their output.(好比搭积木,还是给用户留下最小单位的积木块比较好,不要直接把两个积木插好了,用户就郁闷鸟).
IO Operators Must Be Nonmember Functions
Input and output operators that conform to the conventions of the iostreamlibrary must be ordinary nonmember functions. These operators cannot be members of our own class. If they were, then the left-hand operand would have to be an object of our class type:
Sales_data data;
data << cout; // if operator<< is a member of Sales_data
If these operators are members of any class, they would have to be members of istream or ostream. However, those classes are part of the standard library, and we cannot add members to a class in the library.
Thus,if we want to define the IO operators for our types, we must define them as nonmember functions. Of course, IO operators usually need to read or write the nonpublicdata members. As a consequence, IO operators usually must be declared as friends
Ordinarily the first parameter of an input operator is a reference to the stream from which it is to read, and the second parameter is a reference to the (nonconst) object into which to read. The operator usually returns a reference to its given stream. The second parameter must be nonconstbecause the purpose of an input operator is to read data into this object.
istream &operator>>(istream &is, Sales_data &item)
{
double price; // no need to initialize; we‘ll read into price before we use it
is >> item.bookNo >> item.units_sold >> price;
if (is) // check that the inputs succeeded
item.revenue = item.units_sold * price;
else
item = Sales_data(); // input failed: give the object the default state
return is;
}
The if checks whether the reads were successful. Input operators must deal with the possibility that the input might fail; output operators generally don’t bother.
Errors during Input
The kinds of errors that might happen in an input operator include the following:
?A read operation might fail because the stream contains data of an incorrect type. For example, after reading bookNo, the input operator assumes that the next two items will be numeric data. If nonnumeric data is input, that read and any subsequent use of the stream will fail.
?Any of the reads could hit end-of-file or some other error on the input stream.
Best Practices:Input operators should decide what, if anything, to do about error recovery.
Indicating Errors
Usually an input operator should set only the failbit. Setting eofbit would imply that the file was exhausted, and setting badbit would indicate that the stream was corrupted. These errors are best left to the IO library itself to indicate.
Ordinarily,we define the arithmetic and relational operators as nonmember functions in order to allow conversions for either the left- or right-hand operand (§ 14.1, p.555). These operators shouldn’t need to change the state of either operand, so the parameters are ordinarily references to const.
An arithmetic operator usually generates a new value that is the result of a computation on its two operands. That value is distinct from either operand and is calculated in a local variable. The operation returns a copy of this local as its result. Classes that define an arithmetic operator generally define the corresponding compound assignment operator as well. When a class has both operators, it is usually more efficient to define the arithmetic operator to use compound assignment:
// 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;
}
Classes that define both an arithmetic operator and the related compound assignment ordinarily ought to implement the arithmetic operator by using the compound assignment.
15.3.1 Equality Operators
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() &&lhs.units_sold == rhs.units_sold && lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs);
}
?If a class defines operator==, it should also define operator!=. Users will expect that if they can use ==then they can also use !=, and vice versa.
?One of the equality or inequality operators should delegate the work to the other. That is, one of these operators should do the real work to compare objects. The other should call the one that does the real work.
Because the associative containers and some of the algorithms use the less-than operator, it can be useful to define an operator<.
If a single logical definition for <exists, classes usually should define the < operator. However, if the class also has ==, define <only if the definitions of <and ==yield consistent results.
StrVec &StrVec::operator=(initializer_list<string> il)
{
// alloc_n_copy allocates space and copies elements from the given range
auto data = alloc_n_copy(il.begin(), il.end());
free(); // destroy the elements in this object and free the space
elements = data.first; // update data members to point to the new space
first_free = cap = data.second;
return *this;
}
Compound-Assignment Operators
Compound assignment operators are not required to be members. However, we prefer to define all assignments, including compound assignments, in the class.
Sales_data& Sales_data::operator+=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
Best Practices: Assignment operators must, and ordinarily compound-assignment operators should, be defined as members. These operators should return a reference to the left-hand operand.
If a class has a subscript operator, it usually should define two versions: one that returns a plain reference and the other that is a const member and returns a reference to 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]; }
// other members as in § 13.5 (p. 526)
private:
std::string *elements; // pointer to the first element in the array
};
// 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] = "zero"; // ok: subscript returns a reference to a string
cvec[0] = "Zip"; // error: subscripting cvec returns a reference to const
}
Classes that define increment or decrement operators should define both the prefix and postfix versions. These operators usually should be defined as members.
class StrBlobPtr {
public:
// increment and decrement
StrBlobPtr& operator++(); // prefix operators
StrBlobPtr& operator--();
// other members as before
};
To be consistent with the built-in operators, the prefix operators should return a reference to the incremented or decremented object.
// prefix: return a reference to the incremented/decremented object
StrBlobPtr& StrBlobPtr::operator++()
{
// if curr already points past the end of the container, can‘t increment it
check(curr, "increment past end of StrBlobPtr");
++curr; // advance the current state
return *this;
}
StrBlobPtr& StrBlobPtr::operator--()
{
// if curr is zero, decrementing it will yield an invalid subscript
--curr; // move the current state back one element
check(-1, "decrement past begin of StrBlobPtr");
return *this;
}
class StrBlobPtr {
public:
// increment and decrement
StrBlobPtr operator++(int); // postfix operators
StrBlobPtr operator--(int);
// other members as before
};
To be consistent with the built-in operators, the postfix operators should return the old (unincremented or undecremented) value. That value is returned as a value, not a reference.
The postfix versions have to remember the current state of the object before incrementing the object:
// postfix: increment/decrement the object but return the unchanged value
StrBlobPtr StrBlobPtr::operator++(int)
{
// no check needed here; the call to prefix increment will do the check
StrBlobPtr ret = *this; // save the current value
++*this; // advance one element; prefix ++ checks the increment
return ret; // return the saved state
}
we can explicitly call an overloaded operator as an alternative to using it as an operator in an expression. If we want to call the postfix version using a function call, then we must pass a value for the integer argument:
StrBlob Ptrp(a1); // p points to the vector inside a1
p.operator++(0); // call postfix operator++
p.operator++(); // call prefix operator++
15.7 Member Access Operators
The dereference (*) and arrow (->) operators are often used in classes that represent iterators and in smart pointer classes.
class StrBlobPtr
{
public:
std::string& operator*() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) is the vector to which this object points
}
std::string* operator->() const
{
// delegate the real work to the dereference operator
return & this->operator*();
}
// other members as before
};
Operator arrow must be a member. The dereference operator is not required to be a member but usually should be a member as well.
StrBloba1 = {"hi", "bye", "now"};
StrBlobPtr p(a1); // p points to the vector inside a1
*p = "okay"; // assigns to the first element in a1
cout << p->size() << endl; // prints 4, the size of the first element in a1
cout << (*p).size() << endl; // equivalent to p->size()
15.8 Function-Call Operator
struct absInt {
int operator()(int val) const {
return val < 0 ? -val : val;
}
};
15.8.1 Lambdas Are Function Object
The standard library defines a set of classes that represent the arithmetic, relational, and logical operators. Each class defines a call operator that applies the named operation. For example, the plusclass has a function-call operator that applies +to a pair of operands; the modulusclass defines a call operator that applies the binary % operator; the equal_toclass applies ==;and so on
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
15.8.3 Callable Objects and function
C++ has several kinds of callable objects: functions and pointers to functions, lambdas, objects created by bind, and classes that overload the function-call operator.
function<int(int,int)>
Here we’ve declared a functiontype that can represent callable objects that return an intresult and have two intparameters. We can use that type to represent any of our desk calculator types:
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; };
We can now redefine our mapusing this function type:
// 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
Converting constructors and conversion operators define class-type conversions. Such conversions are also referred to as user-defined conversions.
15.9.1 Conversion Operators
A conversion operator is a special kind of member function that converts a value of a class type to a value of some other type. A conversion function typically has the general form:
operator type() const;
A conversion function must be a member function, may not specify a return type, and must have an empty parameter list. The function usually should be const.
Conversion operators have no explicitly stated return type and no parameters, and they must be defined as member functions. Conversion operations ordinarily should not change the object they are converting. As a result, conversion operators usually should be defined as const members.
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 si;
si = 4; // implicitly converts 4 to SmallInt then calls SmallInt::operator=
si + 3; // implicitly converts si to int followed by integer addition
The new standard introduced explicit conversion operators:
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
Ordinarily,it is a bad idea to define classes with mutual conversions or to define conversions to or from two arithmetic types.
《C++ Primer第五版》读书笔记(12)-Overloaded Operations and Conversions,古老的榕树,5-wow.com
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。