In addition to encapsulating data and methods, classes can also encapsulate operators that make it easy to operate on instances of this class. You can use these operators to perform operations such as assignment or addition on class objects similar to those on integers that you saw in Lesson 5, “Working with Expressions, Statements, and Operators.” Just like functions, operators can also be overloaded.
In this lesson, you learn:
Using the keyword operator
Unary and binary operators
Conversion operators
The move assignment operator
Operators that cannot be redefined
On a syntactical level, there is very little that differentiates an operator from a function, save for the use of the keyword operator
. An operator declaration looks quite like a function declaration:
return_type operator operator_symbol (...parameter list...);
The operator_symbol
in this case could be any of the several operator types that the programmer can define. It could be +
(addition) or &&
(logical AND) and so on. The operands help the compiler distinguish one operator from another. So, why does C++ provide operators when functions are also supported?
Consider a utility class Date
that encapsulates the day, month, and year:
Date holiday (12, 25, 2016); // initialized to Dec 25, 2016
Assuming that you want to add a day and get the instance to contain the next day—Dec 26, 2016—which of the following two options would be more intuitive?
Option 1 (using the increment operator):
++ holiday;
Option 2 (using a member function Increment())
:
holiday.Increment(); // Dec 26, 2016
Clearly, Option 1 scores over method Increment()
. The operator-based mechanism facilitates consumption by supplying ease of use and intuitiveness. Implementing operator (<
) in class Date
would help you compare two instances of class Date
like this:
if(date1 < date2)
{
// Do something
}
else
{
// Do something else
}
Operators can be used in more situations than just classes that manage dates. An addition operator (+
) in a string utility class such as MyString
introduced to you in Listing 9.9 in Lesson 9, “Classes and Objects,” would facilitate easy concatenation:
MyString sayHello ("Hello ");
MyString sayWorld (" world");
MyString sumThem (sayHello + sayWorld); // if operator+ were supported by
MyString
The effort in implementing relevant operators will be rewarded by the ease of consumption of the class.
On a broad level, operators in C++ can be classified into two types: unary operators and binary operators.
As the name suggests, operators that function on a single operand are called unary operators. A unary operator that is implemented in the global namespace or as a static member function of a class uses the following structure:
return_type operator operator_type (parameter_type)
{
// ... implementation
}
A unary operator that is a (non-static) member of a class has a similar structure but is lacking in parameters, because the single parameter that it works upon is the instance of the class itself (*this
):
return_type operator operator_type ()
{
// ... implementation
}
The unary operators that can be overloaded (or redefined) are shown in Table 12.1.
A unary prefix increment operator (++
) can be programmed using the following syntax within the class declaration:
// Unary increment operator (prefix)
Date& operator ++ ()
{
// operator implementation code
return *this;
}
The postfix increment operator (++
), on the other hand, has a different return type and an input parameter (that is not always used):
Date operator ++ (int)
{
// Store a copy of the current state of the object, before incrementing day
Date copy (*this);
// increment implementation code
// Return state before increment (because, postfix)
return copy;
}
The prefix and postfix decrement operators have a similar syntax as the increment operators, just that the declaration would contain a --
where you see a ++
. Listing 12.1 shows a simple class Date
that allows incrementing days using operator (++
).
0: #include <iostream>
1: using namespace std;
2:
3: class Date
4: {
5: private:
6: int day, month, year;
7:
8: public:
9: Date (int inMonth, int inDay, int inYear)
10: : month (inMonth), day(inDay), year (inYear) {};
11:
12: Date& operator ++ () // prefix increment
13: {
14: ++day;
15: return *this;
16: }
17:
18: Date& operator -- () // prefix decrement
19: {
20: --day;
21: return *this;
22: }
23:
24: void DisplayDate()
25: {
26: cout << month << " / " << day << " / " << year << endl;
27: }
28: };
29:
30: int main ()
31: {
32: Date holiday (12, 25, 2016); // Dec 25, 2016
33:
34: cout << "The date object is initialized to: ";
35: holiday.DisplayDate ();
36:
37: ++holiday; // move date ahead by a day
38: cout << "Date after prefix-increment is: ";
39: holiday.DisplayDate ();
40:
41: --holiday; // move date backwards by a day
42: cout << "Date after a prefix-decrement is: ";
43: holiday.DisplayDate ();
44:
45: return 0;
46: }
The date object is initialized to: 12 / 25 / 2016
Date after prefix-increment is: 12 / 26 / 2016
Date after a prefix-decrement is: 12 / 25 / 2016
Analysis
The operators of interest defined in Lines 12 to 22, help in adding or subtracting a day at a time from instances of class Day
, as shown in Lines 37 and 41 in main()
. Prefix increment operators as demonstrated in this sample need to return a reference to the instance after completing the increment operation.
Note
This version of a date class has a bare minimum implementation to reduce lines and to explain how prefix operator (++
) and operator (--
) are to be implemented. A professional version of the same would implement rollover functionalities for month and year and take leap years into consideration as well.
To support postfix increment or decrement, you simply add the following code to class Date
:
// postfix differs from prefix operator in return-type and parameters
Date operator ++ (int) // postfix increment
{
Date copy(month, day, year);
++day;
return copy; // copy of instance before increment returned
}
Date operator -- (int) // postfix decrement
{
Date copy(month, day, year);
--day;
return copy; // copy of instance before decrement returned
}
When your version of class Date
supports both prefix and postfix increment and decrement operators, you will be able to use objects of the class using the following syntax:
Date holiday (12, 25, 2016); // instantiate
++ holiday; // using prefix increment operator++
holiday ++; // using postfix increment operator++
-- holiday; // using prefix decrement operator --
holiday --; // using postfix decrement operator --
As the implementation of the postfix operators demonstrates, a copy containing the existing state of the object is created before the increment or decrement operation to be returned thereafter.
In other words, if you had the choice between using ++ object;
and object ++;
to essentially only increment, you should choose the former to avoid the creation of a temporary copy that will not be used.
If you use Listing 12.1 and insert the following line in main()
:
cout << holiday; // error in absence of conversion operator
The code would result in the following compile failure: error: binary '<<' : no operator found which takes a right-hand operand of type 'Date' (or there is no acceptable conversion)
. This error essentially indicates that cout
doesn’t know how to interpret an instance of Date
as class Date
does not support the operators that convert its contents into a type that cout
would accept.
We know that cout
can work well with a const char*
:
std::cout << "Hello world"; // const char* works!
So, getting cout
to work with an instance of type Date
might be as simple as adding an operator that returns a const char*
version:
operator const char*()
{
// operator implementation that returns a char*
}
Listing 12.2 is a simple implementation of this conversion operator.
0: #include <iostream>
1: #include <sstream> // new include for ostringstream
2: #include <string>
3: using namespace std;
4:
5: class Date
6: {
7: private:
8: int day, month, year;
9: string dateInString;
10:
11: public:
12: Date(int inMonth, int inDay, int inYear)
13: : month(inMonth), day(inDay), year(inYear) {};
14:
15: operator const char*()
16: {
17: ostringstream formattedDate; // assists string construction
18: formattedDate << month << " / " << day << " / " << year;
19:
20: dateInString = formattedDate.str();
21: return dateInString.c_str();
22: }
23: };
24:
25: int main ()
26: {
27: Date Holiday (12, 25, 2016);
28:
29: cout << "Holiday is on: " << Holiday << endl;
30:
31: // string strHoliday (Holiday); // OK!
32: // strHoliday = Date(11, 11, 2016); // also OK!
33:
34: return 0;
35: }
Output
Holiday is on: 12 / 25 / 2016
Analysis
The benefit of implementing operator const char*
as shown in Lines 15 to 23 is visible in Line 29 in main().
Now, an instance of class Date
can directly be used in a cout
statement, taking advantage of the fact that cout
understands const char*
. The compiler automatically uses the output of the appropriate (and in this case, the only available) operator in feeding it to cout
that displays the date on the screen. In your implementation of operator const char*,
you use std::ostringstream
to convert the member integers into a std::string
object as shown in Line 18. You could’ve directly returned formattedDate.str()
, yet you store a copy in private member Date::dateInString
in Line 20 because formattedDate
being a local variable is destroyed when the operator returns. So, the pointer got via str()
would be invalidated on function return.
This operator opens up new possibilities toward consuming class Date
. It allows you to even assign an instance of a Date
directly to a string
:
string strHoliday (holiday);
strHoliday = Date(11, 11, 2016);
Caution
Note that such assignments cause implicit conversions, that is, the compiler has used the available conversion operator (in this case const char*
) thereby permitting unintended assignments that get compiled without error. To avoid implicit conversions, use keyword explicit
at the beginning of an operator declaration, as follows:
explicit operator const char*()
{
// conversion code here
}
Using explicit
would force the programmer to assert his intention to convert using a cast:
string strHoliday(static_cast<const char*>(Holiday));
strHoliday=static_cast<const char*>(Date(11,11,2016));
Casting, including static_cast
, is discussed in detail in Lesson 13, “Casting Operators.”
Note
Program as many operators as you think your class would be used with. If your application needs an integer representation of a Date
, then you may program it as follows:
explicit operator int()
{
return day + month + year;
}
This would allow an instance of Date
to be used or transacted as an integer:
FuncTakesInt(static_cast<int>(Date(12, 25, 2016)));
Listing 12.8 later in this lesson also demonstrates conversion operators used with a string class.
*
) and Member Selection Operator (->
)The dereference operator (*
) and member selection operator (->
) are most frequently used in the programming of smart pointer classes. Smart pointers are utility classes that wrap regular pointers and simplify memory management by resolving ownership and copy issues using operators. In some cases, they can even help improve the performance of the application. Smart pointers are discussed in detail in Lesson 26, “Understanding Smart Pointers.” This lesson takes a brief look at how overloading operators helps in making smart pointers work.
Analyze the use of the std::unique_ptr
in Listing 12.3 and understand how it uses operator (*
) and operator (->
) to help you use the smart pointer class like any normal pointer.
0: #include <iostream>
1: #include <memory> // new include to use unique_ptr
2: using namespace std;
3:
4: class Date
5: {
6: private:
7: int day, month, year;
8: string dateInString;
9:
10: public:
11: Date(int inMonth, int inDay, int inYear)
12: : month(inMonth), day(inDay), year(inYear) {};
13:
14: void DisplayDate()
15: {
16: cout << month << " / " << day << " / " << year << endl;
17: }
18: };
19:
20: int main()
21: {
22: unique_ptr<int> smartIntPtr(new int);
23: *smartIntPtr = 42;
24:
25: // Use smart pointer type like an int*
26: cout << "Integer value is: " << *smartIntPtr << endl;
27:
28: unique_ptr<Date> smartHoliday (new Date(12, 25, 2016));
29: cout << "The new instance of date contains: ";
30:
31: // use smartHoliday just as you would a Date*
32: smartHoliday->DisplayDate();
33:
34: return 0;
35: }
Output
Integer value is: 42
The new instance of date contains: 12 / 25 / 2016
Analysis
Line 22 is where you declare a smart pointer to type int
. This line shows template initialization syntax for smart pointer class unique_ptr
. Similarly, Line 28 declares a smart pointer to an instance of class Date
. Focus on the pattern, and ignore the details for the moment.
Note
Don’t worry if this template syntax looks awkward because templates are introduced later in Lesson 14, “An Introduction to Macros and Templates.”
This example demonstrates how a smart pointer allows you to use normal pointer syntax as shown in Lines 23 and 32. In Line 23, you are able to display the value of the int
using *smartIntPtr
, whereas in Line 32 you use smartHoliday->DisplayData()
as if these two variables were an int*
and Date*
, respectively. The secret lies in the pointer class std::unique_ptr
that is smart because it implements operator (*
) and operator (->
).
Note
Smart pointer classes can do a lot more than just parade around as normal pointers, or de-allocate memory when they go out of scope. Find out more about this topic in Lesson 26.
To see an implementation of a basic smart pointer class that has overloaded these operators, you may briefly visit Listing 26.1.
Operators that function on two operands are called binary operators. The definition of a binary operator implemented as a global function or a static member function is the following:
return_type operator_type (parameter1, parameter2);
The definition of a binary operator implemented as a class member is
return_type operator_type (parameter);
The reason the class member version of a binary operator accepts only one parameter is that the second parameter is usually derived from the attributes of the class itself.
Table 12.2 contains binary operators that can be overloaded or redefined in your C++ application.
a+b
) and Subtraction (a-b
) OperatorsSimilar to the increment/decrement operators, the binary plus and minus, when defined, enable you to add or subtract the value of a supported data type from an object of the class that implements these operators. Take a look at your calendar class Date
again. Although you have already implemented the capability to increment Date
so that it moves the calendar one day forward, you still do not support the capability to move it, say, five days ahead. To do this, you need to implement binary operator (+
), as the code in Listing 12.4 demonstrates.
0: #include <iostream>
1: using namespace std;
2:
3: class Date
4: {
5: private:
6: int day, month, year;
7: string dateInString;
8:
9: public:
10: Date(int inMonth, int inDay, int inYear)
11: : month(inMonth), day(inDay), year(inYear) {};
12:
13: Date operator + (int daysToAdd) // binary addition
14: {
15: Date newDate (month, day + daysToAdd, year);
16: return newDate;
17: }
18:
19: Date operator - (int daysToSub) // binary subtraction
20: {
21: return Date(month, day - daysToSub, year);
22: }
23:
24: void DisplayDate()
25: {
26: cout << month << " / " << day << " / " << year << endl;
27: }
28: };
29:
30: int main()
31: {
32: Date Holiday (12, 25, 2016);
33: cout << "Holiday on: ";
34: Holiday.DisplayDate ();
35:
36: Date PreviousHoliday (Holiday - 19);
37: cout << "Previous holiday on: ";
38: PreviousHoliday.DisplayDate();
39:
40: Date NextHoliday(Holiday + 6);
41: cout << "Next holiday on: ";
42: NextHoliday.DisplayDate ();
43:
44: return 0;
45: }
Holiday on: 12 / 25 / 2016
Previous holiday on: 12 / 6 / 2016
Next holiday on: 12 / 31 / 2016
Analysis
Lines 13 to 22 contain the implementations of the binary operator (+
) and operator (-
) that permit the use of simple addition and subtraction syntax as seen in main()
in Lines 40 and 36, respectively.
The binary addition operator would also be useful in a string class. In Lesson 9, you analyze a simple string wrapper class MyString
that encapsulates memory management, copying, and the like, as shown in Listing 9.9. This class MyString
doesn’t support the concatenation of two strings using a simple syntax:
MyString Hello("Hello ");
MyString World(" World");
MyString HelloWorld(Hello + World); // error: operator+ not defined
Defining this operator (+
) makes using MyString
extremely easy and is hence worth the effort:
MyString operator+ (const MyString& addThis)
{
MyString newString;
if (addThis.buffer != NULL)
{
newString.buffer = new char[GetLength() + strlen(addThis.buffer) + 1];
strcpy(newString.buffer, buffer);
strcat(newString.buffer, addThis.buffer);
}
return newString;
}
Add the preceding code to Listing 9.9 with a private default constructor MyString()
with empty implementation to be able to use the addition syntax. You can see a version of class MyString
with operator (+
) among others in Listing 12.11 later in this lesson.
+=
) and Subtraction Assignment (-=
) OperatorsThe addition assignment operators allow syntax such as “a += b;
” that allows the programmer to increment the value of an object a
by an amount b
. In doing this, the utility of the addition assignment operator is that it can be overloaded to accept different types of parameter b
. Listing 12.5 that follows allows you to add an integer value to a Date
object.
0: #include <iostream>
1: using namespace std;
2:
3: class Date
4: {
5: private:
6: int day, month, year;
7:
8: public:
9: Date(int inMonth, int inDay, int inYear)
10: : month(inMonth), day(inDay), year(inYear) {}
11:
12: void operator+= (int daysToAdd) // addition assignment
13: {
14: day += daysToAdd;
15: }
16:
17: void operator-= (int daysToSub) // subtraction assignment
18: {
19: day -= daysToSub;
20: }
21:
22: void DisplayDate()
23: {
24: cout << month << " / " << day << " / " << year << endl;
25: }
26: };
27:
28: int main()
29: {
30: Date holiday (12, 25, 2016);
31: cout << "holiday is on: ";
32: holiday.DisplayDate ();
33:
34: cout << "holiday -= 19 gives: ";
35: holiday -= 19;
36: holiday.DisplayDate();
37:
38: cout << "holiday += 25 gives: ";
39: holiday += 25;
40: holiday.DisplayDate ();
41:
42: return 0;
43: }
Output
holiday is on: 12 / 25 / 2016
holiday -= 19 gives: 12 / 6 / 2016
holiday += 25 gives: 12 / 31 / 2016
Analysis
The addition and subtraction assignment operators of interest are in Lines 12 to 20. These allow adding and subtracting an integer value for days, as seen in main()
, for instance:
35: holiday -= 19;
39: holiday += 25;
Your class Date
now allows users to add or remove days from it as if they are dealing with integers using addition or subtraction assignment operators that take an int
as a parameter. You can even provide overloaded versions of the addition assignment operator (+=
) that work with an instance of a fictitious class Days
:
// operator that adds a Days to an existing Date
void operator += (const Days& daysToAdd)
{
day += daysToAdd.GetDays();
}
The multiplication assignment *=
, division assignment /=
, modulus assignment %=
, subtraction assignment -=
, left-shift assignment <<=
, right-shift assignment >>=
, XOR assignment ^=
, bitwise inclusive OR assignment |=
, and bitwise AND assignment &=
operators have a syntax similar to the addition assignment operator shown in Listing 12.5.
Although the ultimate objective of overloading operators is making the class easy and intuitive to use, there are many situations where implementing an operator might not make sense. For example, our calendar class Date
has absolutely no use for a bitwise AND assignment &=
operator. No user of this class should ever expect (or even think of) getting useful results from an operation such as greatDay &= 20;
.
==
) and Inequality (!=
) OperatorsWhat do you expect when the user of class Date
compares one instance to another:
if (date1 == date2)
{
// Do something
}
else
{
// Do something else
}
In the absence of an equality operator ==
, the compiler simply performs a binary comparison of the two objects and returns true
when they are exactly identical. This binary comparison will work for instances of classes containing simple data types (like the Date
class as of now), but it will not work if the class in question has a non-static string member (char*
), such as MyString
in Listing 9.9. When two instances of class MyString
are compared, a binary comparison of the member attributes would actually compare the member string pointer values (MyString::buffer
). These would not be equal even when the strings are identical in content. Comparisons involving two instances of MyString
would return false
consistently. You solve this problem by defining comparison operators. A generic expression of the equality operator is the following:
bool operator== (const ClassType& compareTo)
{
// comparison code here, return true if equal else false
}
The inequality operator can reuse the equality operator:
bool operator!= (const ClassType& compareTo)
{
// comparison code here, return true if inequal else false
}
The inequality operator can be the inverse (logical NOT) of the result of the equality operator. Listing 12.6 demonstrates comparison operators defined by our calendar class Date
.
0: #include <iostream>
1: using namespace std;
2:
3: class Date
4: {
5: private:
6: int day, month, year;
7:
8: public:
9: Date(int inMonth, int inDay, int inYear)
10: : month(inMonth), day(inDay), year(inYear) {}
11:
12: bool operator== (const Date& compareTo)
13: {
14: return ((day == compareTo.day)
15: && (month == compareTo.month)
16: && (year == compareTo.year));
17: }
18:
19: bool operator!= (const Date& compareTo)
20: {
21: return !(this->operator==(compareTo));
22: }
23:
24: void DisplayDate()
25: {
26: cout << month << " / " << day << " / " << year << endl;
27: }
28: };
29:
30: int main()
31: {
32: Date holiday1 (12, 25, 2016);
33: Date holiday2 (12, 31, 2016);
34:
35: cout << "holiday 1 is: ";
36: holiday1.DisplayDate();
37: cout << "holiday 2 is: ";
38: holiday2.DisplayDate();
39:
40: if (holiday1 == holiday2)
41: cout << "Equality operator: The two are on the same day" << endl;
42: else
43: cout << "Equality operator: The two are on different days" << endl;
44:
45: if (holiday1 != holiday2)
46: cout << "Inequality operator: The two are on different days" << endl;
47: else
48: cout << "Inequality operator: The two are on the same day" << endl;
49:
50: return 0;
51: }
Output
holiday 1 is: 12 / 25 / 2016
holiday 2 is: 12 / 31 / 2016
Equality operator: The two are on different days
Inequality operator: The two are on different days
Analysis
The equality operator (==
) is a simple implementation that returns true
if the day, month, and year are all equal, as shown in Lines 12 to 17. The inequality operator (!=
) simply reuses the equality operator code as seen in Line 21. The presence of these operators helps compare two Date
objects, holiday1
and holiday2
, in main()
in Lines 40 and 45.
<
, >
, <=
, and >=
OperatorsThe code in Listing 12.6 made the Date
class intelligent enough to be able to tell whether two Date
objects are equal or unequal. You need to program the less-than (<
), greater-than (>
), less-than-equals (<=
), and greater-than-equals (>=
) operators to enable conditional checking akin to the following:
if (date1 < date2) {// do something}
or
if (date1 <= date2) {// do something}
if (date1 > date2) {// do something}
or
if (date1 >= date2) {// do something}
These operators are demonstrated by the code shown in Listing 12.7.
0: #include <iostream>
1: using namespace std;
2:
3: class Date
4: {
5: private:
6: int day, month, year;
7:
8: public:
9: Date(int inMonth, int inDay, int inYear)
10: : month(inMonth), day(inDay), year(inYear) {}
11:
12: bool operator< (const Date& compareTo)
13: {
14: if (year < compareTo.year)
15: return true;
16: else if (month < compareTo.month)
17: return true;
18: else if (day < compareTo.day)
19: return true;
20: else
21: return false;
22: }
23:
24: bool operator<= (const Date& compareTo)
25: {
26: if (this->operator== (compareTo))
27: return true;
28: else
29: return this->operator< (compareTo);
30: }
31:
32: bool operator > (const Date& compareTo)
33: {
34: return !(this->operator<= (compareTo));
35: }
36:
37: bool operator== (const Date& compareTo)
38: {
39: return ((day == compareTo.day)
40: && (month == compareTo.month)
41: && (year == compareTo.year));
42: }
43:
44: bool operator>= (const Date& compareTo)
45: {
46: if(this->operator== (compareTo))
47: return true;
48: else
49: return this->operator> (compareTo);
50: }
51:
52: void DisplayDate()
53: {
54: cout << month << " / " << day << " / " << year << endl;
55: }
56: };
57:
58: int main()
59: {
60: Date holiday1 (12, 25, 2016);
61: Date holiday2 (12, 31, 2016);
62:
63: cout << "holiday 1 is: ";
64: holiday1.DisplayDate();
65: cout << "holiday 2 is: ";
66: holiday2.DisplayDate();
67:
68: if (holiday1 < holiday2)
69: cout << "operator<: holiday1 happens first" << endl;
70:
71: if (holiday2 > holiday1)
72: cout << "operator>: holiday2 happens later" << endl;
73:
74: if (holiday1 <= holiday2)
75: cout << "operator<=: holiday1 happens on or before holiday2" << endl;
76:
77: if (holiday2 >= holiday1)
78: cout << "operator>=: holiday2 happens on or after holiday1" << endl;
79:
80: return 0;
81: }
holiday 1 is: 12 / 25 / 2016
holiday 2 is: 12 / 31 / 2016
operator<: holiday1 happens first
operator>: holiday2 happens later
operator<=: holiday1 happens on or before holiday2
operator>=: holiday2 happens on or after holiday1
Analysis
The operators of interest are implemented in Lines 12 to 50 and partially reuse operator (==
) that you saw in Listing 12.6. The implementation of operators ==
, <
, and >
has been consumed by the rest.
The operators have been consumed inside main()
between Lines 68 and 78, which indicate how easy it now is to compare two different dates.
=
)There are times when you want to assign the contents of an instance of a class to another, like this:
Date holiday(12, 25, 2016);
Date anotherHoliday(1, 1, 2017);
anotherHoliday = holiday; // uses copy assignment operator
This assignment invokes the default copy assignment operator that the compiler has built in to your class when you have not supplied one. Depending on the nature of your class, the default copy assignment operator might be inadequate, especially if your class is managing a resource that will not be copied. This problem with the default copy assignment operator is similar to the one with the default copy constructor discussed in Lesson 9. To ensure deep copies, as with the copy constructor, you need to specify an accompanying copy assignment operator:
ClassType& operator= (const ClassType& copySource)
{
if(this != ©Source) // protection against copy into self
{
// copy assignment operator implementation
}
return *this;
}
Deep copies are important if your class encapsulates a raw pointer, such as class MyString
shown in Listing 9.9. To ensure deep copy during assignments, define a copy assignment operator as shown in Listing 12.8.
0: #include <iostream>
1: using namespace std;
2: #include <string.h>
3: class MyString
4: {
5: private:
6: char* buffer;
7:
8: public:
9: MyString(const char* initialInput)
10: {
11: if(initialInput != NULL)
12: {
13: buffer = new char [strlen(initialInput) + 1];
14: strcpy(buffer, initialInput);
15: }
16: else
17: buffer = NULL;
18: }
19:
20: // Copy assignment operator
21: MyString& operator= (const MyString& copySource)
22: {
23: if ((this != ©Source) && (copySource.buffer != NULL))
24: {
25: if (buffer != NULL)
26: delete[] buffer;
27:
28: // ensure deep copy by first allocating own buffer
29: buffer = new char [strlen(copySource.buffer) + 1];
30:
31: // copy from the source into local buffer
32: strcpy(buffer, copySource.buffer);
33: }
34:
35: return *this;
36: }
37:
38: operator const char*()
39: {
40: return buffer;
41: }
42:
43: ~MyString()
44: {
45: delete[] buffer;
46: }
47: };
48:
49: int main()
50: {
51: MyString string1("Hello ");
52: MyString string2(" World");
53:
54: cout << "Before assignment: " << endl;
55: cout << string1 << string2 << endl;
56: string2 = string1;
57: cout << "After assignment string2 = string1: " << endl;
58: cout << string1 << string2 << endl;
59:
60: return 0;
61: }
Output
Before assignment:
Hello World
After assignment string2 = string1:
Hello Hello
Analysis
I have purposely omitted the copy constructor in this sample to reduce lines of code (but you should be inserting it when programming such a class; refer Listing 9.9 as a reference). The copy assignment operator is implemented in Lines 21 to 36. It is similar in function to a copy constructor and performs a starting check to ensure that the same object is not both the copy source and destination. After the checks return true
, the copy assignment operator for MyString
first deallocates its internal buffer
before reallocating space for the text from the copy source and then uses strcpy()
to copy, as shown in Line 14.
Another subtle change in Listing 12.8 over Listing 9.9 is that you have replaced function GetString()
by operator const char*
as shown in Lines 38 to 41. This operator makes it even easier to use class MyString
, as shown in Line 55, where one cout
statement is used to display two instances of MyString
.
Caution
When implementing a class that manages a dynamically allocated resource such as an array allocated using new
, always ensure that you have implemented (or evaluated the implementation of) the copy constructor and the copy assignment operator in addition to the constructor and the destructor.
Unless you address the issue of resource ownership when an object of your class is copied, your class is incomplete and endangers the stability of the application when used.
Tip
To create a class that cannot be copied, declare the copy constructor and copy assignment operator as private
. Declaration as private
without implementation is sufficient for the compiler to throw error on all attempts at copying this class via passing to a function by value or assigning one instance into another.
[]
)The operator that allow array-style []
access to a class is called subscript operator. The typical syntax of a subscript operator is:
return_type& operator [] (subscript_type& subscript);
So, when creating a class such as MyString
that encapsulates a dynamic array class of characters in a char* buffer
, a subscript operator makes it really easy to randomly access individual characters in the buffer:
class MyString
{
// ... other class members
public:
/*const*/ char& operator [] (int index) /*const*/
{
// return the char at position index in buffer
}
};
The sample in Listing 12.9 demonstrates how the subscript operator ([]
) helps the user in iterating through the characters contained in an instance of MyString
using normal array semantics.
0: #include <iostream>
1: #include <string>
2: #include <string.h>
3: using namespace std;
4: class MyString
5: {
6: private:
7: char* buffer;
8:
9: // private default constructor
10: MyString() {}
11:
12: public:
13: // constructor
14: MyString(const char* initialInput)
15: {
16: if(initialInput != NULL)
17: {
18: buffer = new char [strlen(initialInput) + 1];
19: strcpy(buffer, initialInput);
20: }
21: else
22: buffer = NULL;
23: }
24:
25: // Copy constructor: insert from Listing 9.9 here
26: MyString(const MyString& copySource);
27:
28: // Copy assignment operator: insert from Listing 12.8 here
29: MyString& operator= (const MyString& copySource);
30:
31: const char& operator[] (int index) const
32: {
33: if (index < GetLength())
34: return buffer[index];
35: }
36:
37: // Destructor
38: ~MyString()
39: {
40: if (buffer != NULL)
41: delete [] buffer;
42: }
43:
44: int GetLength() const
45: {
46: return strlen(buffer);
47: }
48:
49: operator const char*()
50: {
51: return buffer;
52: }
53: };
54:
55: int main()
56: {
57: cout << "Type a statement: ";
58: string strInput;
59: getline(cin, strInput);
60:
61: MyString youSaid(strInput.c_str());
62:
63: cout << "Using operator[] for displaying your input: " << endl;
64: for(int index = 0; index < youSaid.GetLength(); ++index)
65: cout << youSaid[index] << " ";
66: cout << endl;
67:
68: cout << "Enter index 0 - " << youSaid.GetLength() - 1 << ": ";
69: int index = 0;
70: cin >> index;
71: cout << "Input character at zero-based position: " << index;
72: cout << " is: "<< youSaid[index] << endl;
73:
74: return 0;
75: }
Output
Type a statement: Hey subscript operators[] are fabulous
Using operator[] for displaying your input:
H e y s u b s c r i p t o p e r a t o r s [ ] a r e f a b u l o u s
Enter index 0 - 37: 2
Input character at zero-based position: 2 is: y
This is just a fun program that takes a sentence you input, constructs a MyString
using it, as shown in Line 61, and then uses a for
loop to print the string character by character with the help of the subscript operator ([]
) using an array-like syntax, as shown in Lines 64 and 65. The operator ([]
) itself is defined in Lines 31 to 35 and supplies direct access to the character at the specified position after ensuring that the requested position is not beyond the end of the char* buffer
.
Caution
Using keyword const
is important even when programming operators. Note how Listing 12.9 has restricted the return value of subscript operator []
to const char&
. The program works and compiles even without the const
keywords, yet the reason you have it there is to avoid this code:
MyString sayHello("Hello World");
sayHello[2] = 'k'; //error: operator[] is const
By using const
you are protecting internal member MyString::buffer
from direct modifications from the outside via operator []
. In addition to classifying the return value as const
, you even have restricted the operator function type to const
to ensure that it cannot modify the class’s member attributes.
In general, use the maximum possible const
restriction to avoid unintentional data modifications and increase protection of the class’s member attributes.
When implementing subscript operators, you can improve on the version shown in Listing 12.9. That one is an implementation of a single subscript operator that works for both reading from and writing to the slots in the dynamic array.
You can, however, implement two subscript operators—one as a const
function and the other as a non-const
one:
char& operator [] (int index); // use to write / change buffer at index
char& operator [] (int index) const; // used only for accessing char at index
The compiler will invoke the const function for read operations and the non-const
version for operations that write into the MyString
object. Thus, you can (if you want to) have separate functionalities in the two subscript operations. There are other binary operators (listed in Table 12.2) that can be redefined or overloaded, but that are not discussed further in this lesson. Their implementation, however, is similar to those that have already been discussed.
Other operators, such as the logical operators and the bitwise operators, need to be programmed if the purpose of the class would be enhanced by having them. Clearly, a calendar class such as Date
does not necessarily need to implement logical operators, whereas a class that performs string and numeric functions might need them frequently.
Keep the objective of your class and its use in perspective when overloading operators or writing new ones.
()
The operator ()
that make objects behave like a function is called a function operator. They find application in the standard template library (STL) and are typically used in STL algorithms. Their usage can include making decisions; such function objects are typically called unary or binary predicates, depending on the number of operands they work on. Listing 12.10 analyzes a really simple function object so you can first understand what gives them such an intriguing name!
1: #include <iostream>
2: #include <string>
3: using namespace std;
4:
5: class Display
6: {
7: public:
8: void operator () (string input) const
9: {
10: cout << input << endl;
11: }
12: };
13:
14: int main ()
15: {
16: Display displayFuncObj;
17:
18: // equivalent to displayFuncObj.operator () ("Display this string! ");
19: displayFuncObj ("Display this string! ");
20:
21: return 0;
22: }
Display this string!
Analysis
Lines 8 to 11 implement operator()
that is then used inside the function main()
at Line 19. Note how the compiler allows the use of object displayFuncObj
as a function
in Line 19 by implicitly converting what looks like a function call to a call to operator()
.
Hence, this operator is also called the function operator ()
, and the object of Display
is also called a function object or functor. This topic is discussed exhaustively in Lesson 21, “Understanding Function Objects.”
The move constructor and the move assignment operators are performance optimization features that have become a part of the standard in C++11, ensuring that temporary values (rvalues that don’t exist beyond the statement) are not wastefully copied. This is particularly useful when handling a class that manages a dynamically allocated resource, such as a dynamic array class or a string class.
Take a look at the addition operator+
as implemented in Listing 12.4. Notice that it actually creates a copy and returns it. If class MyString
as demonstrated in Listing 12.9 supported the addition operator+
, the following lines of code would be valid examples of easy string concatenation:
MyString Hello("Hello ");
MyString World("World");
MyString CPP(" of C++");
MyString sayHello(Hello + World + CPP); // operator+, copy constructor
MyString sayHelloAgain ("overwrite this");
sayHelloAgain = Hello + World + CPP; // operator+, copy constructor, copy
assignment operator=
This simple construct that makes concatenating three strings easy, uses the binary addition operator+
:
MyString operator+ (const MyString& addThis)
{
MyString newStr;
if (addThis.buffer != NULL)
{
// copy into newStr
}
return newStr; // return copy by value, invoke copy constructor
}
While making it easy to concatenate the strings, the addition operator+
can cause performance problems. The creation of sayHello
requires the execution of the addition operator twice. Each execution of operator+
results in the creation of a temporary copy as a MyString
is returned by value, thus causing the execution of the copy constructor. The copy constructor executes a deep copy—to a temporary value that does not exist after the expression. Thus, this expression results in temporary copies (rvalues, for the purists) that are not ever required after the statement and hence are a performance bottleneck forced by C++. Well, until recently at least.
This problem has now finally been resolved in C++11 in which the compiler specifically recognizes temporaries and uses move constructors and move assignment operators, where supplied by the programmer.
The syntax of the move constructor is as follows:
class Sample
{
private:
Type* ptrResource;
public:
Sample(Sample&& moveSource) // Move constructor, note &&
{
ptrResource = moveSource.ptrResource; // take ownership, start move
moveSource.ptrResource = NULL;
}
Sample& operator= (Sample&& moveSource)//move assignment operator, note &&
{
if(this != &moveSource)
{
delete [] ptrResource; // free own resource
ptrResource = moveSource.ptrResource; // take ownership, start move
moveSource.ptrResource = NULL; // free move source of ownership
}
}
Sample(); // default constructor
Sample(const Sample& copySource); // copy constructor
Sample& operator= (const Sample& copySource); // copy assignment
};
Thus, the declaration of the move constructor and assignment operator are different from the regular copy constructor and copy assignment operator in that the input parameter is of type Sample&&
. Additionally, as the input parameter is the move-source, it cannot be a const
parameter as it is modified. Return values remain the same, as these are overloaded versions of the constructor and the assignment operator, respectively.
C++11 compliant compilers ensure that for rvalue temporaries the move constructor is used instead of the copy constructor and the move assignment operator is invoked instead of the copy assignment operator. In your implementation of these two, you ensure that instead of copying, you are simply moving the resource from the source to the destination. Listing 12.11 demonstrates the effectiveness of these two recent additions in optimizing class MyString
.
0: #include <iostream>
1: #include <string.h>
2: using namespace std;
3: class MyString
4: {
5: private:
6: char* buffer;
7:
8: MyString(): buffer(NULL) // private default constructor
9: {
10: cout << "Default constructor called" << endl;
11: }
12:
13: public:
14: MyString(const char* initialInput) // constructor
15: {
16: cout << "Constructor called for: " << initialInput << endl;
17: if(initialInput != NULL)
18: {
19: buffer = new char [strlen(initialInput) + 1];
20: strcpy(buffer, initialInput);
21: }
22: else
23: buffer = NULL;
24: }
25:
26: MyString(MyString&& moveSrc) // move constructor
27: {
28: cout << "Move constructor moves: " << moveSrc.buffer << endl;
29: if(moveSrc.buffer != NULL)
30: {
31: buffer = moveSrc.buffer; // take ownership i.e. 'move'
32: moveSrc.buffer = NULL; // free move source
33: }
34: }
35:
36: MyString& operator= (MyString&& moveSrc) // move assignment op.
37: {
38: cout << "Move assignment op. moves: " << moveSrc.buffer << endl;
39: if((moveSrc.buffer != NULL) && (this != &moveSrc))
40: {
41: delete[] buffer; // release own buffer
42:
43: buffer = moveSrc.buffer; // take ownership i.e. 'move'
44: moveSrc.buffer = NULL; // free move source
45: }
46:
47: return *this;
48: }
49:
50: MyString(const MyString& copySrc) // copy constructor
51: {
52: cout << "Copy constructor copies: " << copySrc.buffer << endl;
53: if (copySrc.buffer != NULL)
54: {
55: buffer = new char[strlen(copySrc.buffer) + 1];
56: strcpy(buffer, copySrc.buffer);
57: }
58: else
59: buffer = NULL;
60: }
61:
62: MyString& operator= (const MyString& copySrc) // Copy assignment op.
63: {
64: cout << "Copy assignment op. copies: " << copySrc.buffer << endl;
65: if ((this != ©Src) && (copySrc.buffer != NULL))
66: {
67: if (buffer != NULL)
68: delete[] buffer;
69:
70: buffer = new char[strlen(copySrc.buffer) + 1];
71: strcpy(buffer, copySrc.buffer);
72: }
73:
74: return *this;
75: }
76:
77: ~MyString() // destructor
78: {
79: if (buffer != NULL)
80: delete[] buffer;
81: }
82:
83: int GetLength()
84: {
85: return strlen(buffer);
86: }
87:
88: operator const char*()
89: {
90: return buffer;
91: }
92:
93: MyString operator+ (const MyString& addThis)
94: {
95: cout << "operator+ called: " << endl;
96: MyString newStr;
97:
98: if (addThis.buffer != NULL)
99: {
100: newStr.buffer = new char[GetLength()+strlen(addThis.buffer)+1];
101: strcpy(newStr.buffer, buffer);
102: strcat(newStr.buffer, addThis.buffer);
103: }
104:
105: return newStr;
106: }
107: };
108:
109: int main()
110: {
111: MyString Hello("Hello ");
112: MyString World("World");
113: MyString CPP(" of C++");
114:
115: MyString sayHelloAgain ("overwrite this");
116: sayHelloAgain = Hello + World + CPP;
117:
118: return 0;
119: }
Output without the move constructor and move assignment operator (by commenting out Lines 26 to 48):
Constructor called for: Hello
Constructor called for: World
Constructor called for: of C++
Constructor called for: overwrite this
operator+ called:
Default constructor called
Copy constructor copies: Hello World
operator+ called:
Default constructor called
Copy constructor copies: Hello World of C++
Copy assignment op. copies: Hello World of C++
Output with the move constructor and move assignment operator enabled:
Constructor called for: Hello
Constructor called for: World
Constructor called for: of C++
Constructor called for: overwrite this
operator+ called:
Default constructor called
Move constructor moves: Hello World
operator+ called:
Default constructor called
Move constructor moves: Hello World of C++
Move assignment op. moves: Hello World of C++
Analysis
This might be a really long code sample, but most of it has already been demonstrated in previous examples and lessons. The most important part of this listing is in Lines 26 to 48 that implement the move constructor and the move assignment operator, respectively. Parts of the output that have been influenced by this new addition to C++11 has been marked in bold. Note how the output changes drastically when compared against the same class without these two entities. If you look at the implementation of the move constructor and the move assignment operator again, you see that the move semantic is essentially implemented by taking ownership of the resources from the move source moveSrc
as shown in Line 31 in the move constructor and Line 43 in the move assignment operator. This is immediately followed by assigning NULL to the move source pointer as shown in Lines 32 and 44. This assignment to NULL ensures that the destructor of the instance that is the move source essentially does no memory deallocation via delete in Line 80 as the ownership has been moved to the destination object. Note that in the absence of the move constructor, the copy constructor is called that does a deep copy of the pointed string. Thus, the move constructor has saved a good amount of processing time in reducing unwanted memory allocations and copy steps.
Programming the move constructor and the move assignment operator is completely optional. Unlike the copy constructor and the copy assignment operator, the compiler does not add a default implementation for you.
Use this feature to optimize the functioning of classes that point to dynamically allocated resources that would otherwise be deep copied even in scenarios where they’re only required temporarily.
Literal constants were introduced in Lesson 3, “Using Variables, Declaring Constants.” Here are some examples of a few:
int bankBalance = 10000;
double pi = 3.14;
char firstAlphabet = ‘a’;
const char* sayHello = "Hello!";
In the preceding code, 10000
, 3.14
, ‘a’
, and "Hello!"
are all literal constants! C++11 extended the standard’s support of literals by allowing you to define your own literals. For instance, if you were working on a scientific application that deals with thermodynamics, you may want all your temperatures to be stored and operated using a scale called Kelvin. You may now declare all your temperatures using a syntax similar to the following:
Temperature k1 = 32.15_F;
Temperature k2 = 0.0_C;
Using literals _F
and _C
that you have defined, you have made your application a lot simpler to read and therefore maintain.
To define your own literal, you define operator ""
like this:
ReturnType operator "" YourLiteral(ValueType value)
{
// conversion code here
}
Depending on the nature of the user defined literal, the ValueType
parameter would be restricted to one of the following:
unsigned long long int
for integral literal
long double
for floating point literal
char
, wchar_t
, char16_t
, and char32_t
for character literal
const char
* for raw string literal
const char
* together with size_t
for string literal
const wchar_t
* together with size_t
for string literal
const char16_t
* together with size_t
for string literal
const char32_t
* together with size_t
for string literal
Listing 12.12 demonstrates a user defined literal that converts types.
0: #include <iostream>
1: using namespace std;
2:
3: struct Temperature
4: {
5: double Kelvin;
6: Temperature(long double kelvin) : Kelvin(kelvin) {}
7: };
8:
9: Temperature operator"" _C(long double celcius)
10: {
11: return Temperature(celcius + 273);
12: }
13:
14: Temperature operator "" _F(long double fahrenheit)
15: {
16: return Temperature((fahrenheit + 459.67) * 5 / 9);
17: }
18:
19: int main()
20: {
21: Temperature k1 = 31.73_F;
22: Temperature k2 = 0.0_C;
23:
24: cout << "k1 is " << k1.Kelvin << " Kelvin" << endl;
25: cout << "k2 is " << k2.Kelvin << " Kelvin" << endl;
26:
27: return 0;
28: }
Output
k1 is 273 Kelvin
k2 is 273 Kelvin
Analysis
Lines 21 and 22 in the sample above initialize two instances of Temperature
, one using a user defined literal _F
to declare an initial value in Fahrenheit and the other using a user defined literal to declare an initial value in Celcius (also called Centigrade). The two literals are defined in Lines 9–17, and do the work of converting the respective units into Kelvin and returning an instance of Temperature
. Note that k2
has intentionally been initialized to 0.0_C
and not to 0_C
, because the literal _C
has been defined (and is required) to take a long double
as input value and 0 would’ve interpreted as an integer.
With all the flexibility that C++ gives you in customizing the behavior of the operators and making your classes easy to use, it still keeps some cards to itself by not allowing you to change or alter the behavior of some operators that are expected to perform consistently. The operators that cannot be redefined are shown in Table 12.3.
You learned how programming operators can make a significant difference to the ease with which your class can be consumed. When programming a class that manages a resource, for example a dynamic array or a string, you need to supply a copy constructor and copy assignment operator for a minimum, in addition to a destructor. A utility class that manages a dynamic array can do very well with a move constructor and a move assignment operator that ensures that the contained resource is not deep-copied for temporary objects. Last but not least, you learned that operators such as .
, .*
, ::
, ?:
, and sizeof
cannot be redefined.
Q My class encapsulates a dynamic array of integers. What functions and operators should I implement for a minimum?
A When programming such a class, you need to clearly define the behavior in the scenario where an instance is being copied directly into another via assignment or copied indirectly by being passed to a function by value. You typically implement the copy constructor, copy assignment operator, and the destructor. You also implement the move constructor and move assignment operator if you want to tweak the performance of this class in certain cases. To enable an array-like access to elements stored inside an instance of the class, you would want to overload the subscript operator[]
.
Q I have an instance object of a class. I want to support this syntax: cout << object;.What operator do I need to implement?
A You need to implement a conversion operator that allows your class to be interpreted as a type that std::cout
can handle upfront. One way is to define operator char*()
as you also did in Listing 12.2.
Q I want to create my own smart pointer class. What functions and operators do I need to implement for a minimum?
A A smart pointer needs to supply the ability of being used as a normal pointer as in *pSmartPtr
or pSmartPtr->Func()
. To enable this you implement operator (*
) and operator (->
). In addition, for it to be smart, you also take care of automatic resource release/returns by programming the destructor accordingly, and you would clearly define how copy or assignment works by implementing the copy constructor and copy assignment operator or by prohibiting it by declaring these two as private
.
The Workshop contains quiz questions to help solidify your understanding of the material covered and exercises to provide you with experience in using what you’ve learned. Try to answer the quiz and exercise questions before checking the answers in Appendix E, and be certain you understand the answers before going to the next lesson.
1. Can my subscript operator []
return const
and non-const
variants of return types?
const Type& operator[](int index);
Type& operator[](int index); // is this OK?
2. Would you ever declare the copy constructor or copy assignment operator as private
?
3. Would it make sense to define a move constructor and move assignment operator for your class Date
?
1. Program a conversion operator for class Date
that converts the date it holds into a unique integer.
2. Program a move constructor and move assignment operator for class DynIntegers
that encapsulates a dynamically allocated array in the form of private member int*
.
18.191.242.62