Self Assignment Check C 47

Operator overloading means that the operation performed by the operator depends on the type of operands provided to the operator. For example, (a) the bit left-shift operator is overloaded to perform stream insertion if the left operand is a object such as ; (b) the operator could means multiplication for two numbers of built-in types or indirection if it operates on an address. C++ lets you extend operator overloading to user-defined types (classes).

Operator overloading is similar to function overloading, where you have many versions of the same function differentiated by their parameter lists.

Overloaded Operators in the string class

As an example, the C++ class (in header ) overloads these operators to work on objects:

  • String comparison (, , , , , ): For example, you can use to compare the contents of two objects.
  • Stream insertion and extraction (, ): For example, you can use and to output/input objects.
  • Strings concatenation (, ): For example, concatenates two objects to produce a new object; appends into .
  • Character indexing or subscripting : For example, you can use to get the at index ; or to modify the at index . Take note that operator does not perform index-bound check, i.e., you have to ensure that the index is within the bounds. To perform index-bound check, you can use 's member function.
  • Assignment (): For example, assigns into .
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <iomanip> #include <string> using namespace std; int main() { string msg1("hello"); string msg2("HELLO"); string msg3("hello"); cout << boolalpha; cout << (msg1 == msg2) << endl; cout << (msg1 == msg3) << endl; cout << (msg1 < msg2) << endl; string msg4 = msg1; cout << msg4 << endl; cout << (msg1 + " " + msg2) << endl; msg3 += msg2; cout << msg3 << endl; cout << msg1[1] << endl; cout << msg1[99] << endl; }

Notes: The relational operators (, , , , , ), , , are overloaded as non-member functions, where the left operand could be a non- object (such as C-string, , ); while , , are overloaded as member functions where the left operand must be a object. I shall elaborate later.

User-defined Operator Overloading

"operator" Functions

To overload an operator, you use a special function form called an operator function, in the form of , where denotes the operator to be overloaded:

return-typeoperatorΔ(parameter-list)

For example, overloads the operator; overloads the operator. Take note that must be an existing C++ operator. You cannot create you own operator.

Example: Overloading '+' Operator for the Point Class as Member Function

In this example, we shall overload the operator in the class to support addition of two objects. In other words, we can write , where , and are objects, similar to the usual arithmetic operation. We shall construct a new instance for the sum, without changing the and instances.

Point.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #ifndef POINT_H #define POINT_H class Point { private: int x, y; public: Point(int x = 0, int y = 0); int getX() const; int getY() const; void setX(int x); void setY(int y); void print() const; const Point operator+(const Point & rhs) const; }; #endif

Program Notes:

  • We overload the operator via a member function , which shall add this instance (left operand) with the operand, construct a new instance containing the sum and and return it by value. We cannot return by reference a local variable created inside the function, as the local variable would be destroyed when the function exits.
  • The operand is passed by reference for performance.
  • The member function is declared , which cannot modify data members.
  • The return value is declared , so as to prevent it from being used as lvalue. For example, it prevents writing , which is meaningless and could be due to misspelling .
Point.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include "Point.h" #include <iostream> using namespace std; Point::Point(int x, int y) : x(x), y(y) { } int Point::getX() const { return x; } int Point::getY() const { return y; } void Point::setX(int x) { this->x = x; } void Point::setY(int y) { this->y = y; } void Point::print() const { cout << "(" << x << "," << y << ")" << endl; } const Point Point::operator+(const Point & rhs) const { return Point(x + rhs.x, y + rhs.y); }

Program Notes:

  • The function allocates a new object with the sums of 's and 's, and returns this object by value.
TestPoint.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "Point.h" #include <iostream> using namespace std; int main() { Point p1(1, 2), p2(4, 5); Point p3 = p1 + p2; p1.print(); p2.print(); p3.print(); Point p4 = p1.operator+(p2); p4.print(); Point p5 = p1 + p2 + p3 + p4; p5.print(); }

Program Notes:

  • You can invoke the overloaded operator via , which will be translated into the dot operation .
  • The operator supports chaining (cascading) operations, as returns a object.

Restrictions on Operator Overloading

  • The overloaded operator must be an existing and valid operator. You cannot create your own operator such as ⊕.
  • Certain C++ operators cannot be overloaded, such as , dot ( and ), scope resolution () and conditional ().
  • The overloaded operator must have at least one operands of the user-defined types. You cannot overload an operator working on fundamental types. That is, you can't overload the operator for two s (fundamental type) to perform subtraction.
  • You cannot change the syntax rules (such as associativity, precedence and number of arguments) of the overloaded operator.

Overloading Operator via "friend" non-member function

Why can't we always use Member Function for Operator Overloading?

Themember function can only be invoked from an object via the dot operator, e.g., , which is equivalent to . Clearly the left operand should be an object of that particular class. Suppose that we want to overload a binary operator such as to multiply the object with an literal, can be translated into , but cannot be represented using member function. One way to deal with this problem is only allow user to write but not , which is not user friendly and break the rule of commutativity. Another way is to use a non-member function, which does not invoke through an object and dot operator, but through the arguments provided. For example, could be translated to .

In brief, you cannot use member function to overload an operator if the left operand is not an object of that particular class.

"friend" Functions

A regular non-member function cannot directly access the private data of the objects given in its arguments. A special type of function, called s, are allowed to access the private data.

A "friend" function of a class, marked by the keyword , is a function defined outside the class, yet its argument of that class has unrestricted access to all the class members (, and data members and member functions). Friend functions can enhance the performance, as they eliminate the need of calling public member functions to access the private data members.

Example: Overloading << and >> Operators of Point class using non-member friend Functions

In this example, we shall overload and operators to support stream insertion and extraction of objects, i.e., , and . Since the left operand is not a object ( is an object and is an object), we cannot use member function, but need to use non-memberfunction for operator overloading. We shall make these functions s of the class, to allow them to access the private data members directly for enhanced performance.

Point.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #ifndef POINT_H #define POINT_H #include <iostream> class Point { private: int x, y; public: Point(int x = 0, int y = 0); int getX() const; int getY() const; void setX(int x); void setY(int y); friend std::ostream & operator<<(std::ostream & out, const Point & point); friend std::istream & operator>>(std::istream & in, Point & point); }; #endif

Program Notes:

  • Friends are neither or , and can be listed anywhere within the class declaration.
  • The and need to be passed into the function by reference, so that the function accesses the and directly (instead of a clone copy by value).
  • We return the and passed into the function by reference too, so as to support cascading operations. For example, will be interpreted as .
  • In , the reference parameter is declared as . Hence, the function cannot modify the object. On the other hand, in , the reference is non-const, as it will be modified to keep the input.
  • We use fully-qualified name instead of placing a "" statement in the header. It is because this header could be included in many files, which would include the statement too and may not be desirable.
Point.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> #include "Point.h" using namespace std; Point::Point(int x, int y) : x(x), y(y) { } int Point::getX() const { return x; } int Point::getY() const { return y; } void Point::setX(int x) { this->x = x; } void Point::setY(int y) { this->y = y; } ostream & operator<<(ostream & out, const Point & point) { out << "(" << point.x << "," << point.y << ")"; return out; } istream & operator>>(istream & in, Point & point) { cout << "Enter x and y coord: "; in >> point.x >> point.y; return in; }

Program Notes:

  • The function definition does not require the keyword , and the scope resolution qualifier, as it does not belong to the class.
  • The function is declared as a friend of class. Hence, it can access the private data members and of its argument directly. function is NOT a friend of class, as there is no need to access the private member of .
  • Instead of accessing private data member and directly, you could use public member function and . In this case, there is no need to declare as a friend of the class. You could simply declare a regular function prototype in the header. ostream & operator<<(ostream & out, const Point & point); ostream & operator<<(ostream & out, const Point & point) { out << "(" << point.getX() << "," << point.getY() << ")"; return out; } Using is recommended, as it enhances performance. Furthermore, the overloaded operator becomes part of the extended public interface of the class, which helps in ease-of-use and ease-of-maintenance.
TestPoint.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> #include "Point.h" using namespace std; int main() { Point p1(1, 2), p2; cout << p1 << endl; operator<<(cout, p1); cout << endl; cin >> p1; cout << p1 << endl; operator>>(cin, p1); cout << p1 << endl; cin >> p1 >> p2; cout << p1 << endl; cout << p2 << endl; }

The overloaded and can also be used for file input/output, as the file IO stream (in header) is a subclass of . For example,

#include <fstream> #include "Point.h" using namespace std; int main() { Point p1(1, 2); ofstream fout("out.txt");fout << p1 << endl; ifstream fin("in.txt");fin >> p1; cout << p1 << endl; }

Overloading Binary Operators

All C++ operators are either binary (e.g., ) or unary (e.g. , ), with the exception of tenary conditional operator () which cannot be overloaded.

Suppose that we wish to overload the binary operator to compare two objects. We could do it as a member function or non-member function.

  1. To overload as a member function, the declaration is as follows: class Point { public: bool operator==(const Point & rhs) const; ...... }; The compiler translates "" to "", as a member function call of object , with argument .

    Member function can only be used if the left operand is an object of that particular class.

  2. To overload as a non-member function, which is often declared as a to access the private data for enhanced performance, the declaration is as follows: class Point { friend bool operator==(const Point & lhs, const Point & rhs); ...... }; The compiler translates the expression "" to "".

Overloading Unary Operators

Most of the unary operators are prefix operators, e.g., , . Hence, prefix is the norm for unary operators. However, unary increment and decrement come in two forms: prefix (, ) and postfix (, ). We to a mechanism to differentiate the two forms.

Unary Prefix Operator

Example of unary prefix operators are , , and . You could do it as a non-member function as well as member function. For example, to overload the prefix increment operator :

  1. To overload as a non-member function: class Point { friend Point & operator++(Point & point); ...... }; The compiler translates "" to "".
  2. To overload as a member function: class Point { public: Point & operator++(); ...... }; The compiler translates "" to "".

You can use either member function or non-member friend function to overload unary operators, as their only operand shall be an object of that class.

Unary Postfix Operator

The unary increment and decrement operators come in two forms: prefix (, ) and postfix (, ). Overloading postfix operators (such as , x--) present a challenge. It ought to be differentiated from the prefix operator (, ). A "dummy" argument is therefore introduced to indicate postfix operation as shown below. Take note that postfix shall save the old value, perform the increment, and then return the saved value by value.

  1. To overload as non-member function: class Point { friend const Point operator++(Point & point, int dummy); }; The compiler translates "" to "". The argument is strictly a dummy value to differentiate prefix from postfix operation.
  2. To overload as a member function: class Point { public: const Point operator++(int dummy); ...... }; The compiler translates "" to "".

Example: Overloading Prefix and Postfix ++ for the Counter Class

Counter.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #ifndef COUNTER_H #define COUNTER_H #include <iostream> class Counter { private: int count; public: Counter(int count = 0); int getCount() const; void setCount(int count);Counter & operator++();const Counter operator++(int dummy); friend std::ostream & operator<<(std::ostream & out, const Counter & counter); }; #endif

Program Notes:

  • The prefix function returns a reference to this instance, to support chaining (or cascading), e.g., as . However, the return reference can be used as lvalue with unexpected operations (e.g., ).
  • The postfix function returns a object by value. A value cannot be used as lvalue. This prevents chaining such as . Although it would be interpreted as . However, does not return this object, but an temporary object. The subsequent works on the temporary object.
  • Both prefix and postfix functions are non-, as they modify the data member .
Counter.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include "Counter.h" #include <iostream> using namespace std; Counter::Counter(int c) : count(c) { } int Counter::getCount() const { return count; } void Counter::setCount(int c) { count = c; } Counter & Counter::operator++() { ++count; return *this; }const Counter Counter::operator++(int dummy) { Counter old(*this); ++count; return old; } ostream & operator<<(ostream & out, const Counter & counter) { out << counter.count; return out; }

Program Notes:

  • The prefix function increments the , and returns this object by reference.
  • The postfix function saves the old value (by constructing a new instance with this object via the copy constructor), increments the , and return the saved object by value.
  • Clearly, postfix operation on object is less efficient than the prefix operation, as it create a temporary object. If there is no subsequent operation that relies on the output of prefix/postfix operation, use prefix operation.
TestCounter.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include "Counter.h" #include <iostream> using namespace std; int main() { Counter c1; cout << c1 << endl; cout << ++c1 << endl; cout << c1 << endl; cout << c1++ << endl; cout << c1 << endl; cout << ++++c1 << endl; cout << c1++++ << endl; }

Program Notes:

  • Take note of the difference in and . Both prefix and postfix operators work as expected.
  • is allowed and works correctly. is disallowed, because it would produce incorrect result.

Example: Putting them together in Point Class

This example overload binary operator and as non-member functions for stream insertion and stream extraction. It also overload unary (postfix and prefix) and binary as member function; and , operators.

Point.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #ifndef POINT_H #define POINT_H #include <iostream> class Point { private: int x, y; public: explicit Point(int x = 0, int y = 0); int getX() const; int getY() const; void setX(int x); void setY(int y); Point & operator++(); const Point operator++(int dummy); const Point operator+(const Point & rhs) const; const Point operator+(int value) const; Point & operator+=(int value); Point & operator+=(const Point & rhs); friend std::ostream & operator<<(std::ostream & out, const Point & point); friend std::istream & operator>>(std::istream & in, Point & point); friend const Point operator+(int value, const Point & rhs); }; #endif
Point.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include "Point.h" #include <iostream> using namespace std; Point::Point(int x, int y) : x(x), y(y) { } int Point::getX() const { return x; } int Point::getY() const { return y; } void Point::setX(int x) { this->x = x; } void Point::setY(int y) { this->y = y; } Point & Point::operator++() { ++x; ++y; return *this; } const Point Point::operator++(int dummy) { Point old(*this); ++x; ++y; return old; } const Point Point::operator+(int value) const { return Point(x + value, y + value); } const Point Point::operator+(const Point & rhs) const { return Point(x + rhs.x, y + rhs.y); } Point & Point::operator+=(int value) { x += value; y += value; return *this; } Point & Point::operator+=(const Point & rhs) { x += rhs.x; y += rhs.y; return *this; } ostream & operator<<(ostream & out, const Point & point) { out << "(" << point.x << "," << point.y << ")"; return out; } istream & operator>>(istream & in, Point & point) { cout << "Enter x and y coord: "; in >> point.x >> point.y; return in; } const Point operator+(int value, const Point & rhs) { return rhs + value; }
TestPoint.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> #include "Point.h" using namespace std; int main() { Point p1(1, 2); cout << p1 << endl; Point p2(3,4); cout << p1 + p2 << endl; cout << p1 + 10 << endl; cout << 20 + p1 << endl; cout << 10 + p1 + 20 + p1 << endl; p1 += p2; cout << p1 << endl; p1 += 3; cout << p1 << endl; Point p3; cout << p3++ << endl; cout << p3 << endl; cout << ++p3 << endl; }

Implicit Conversion via Single-argument Constructor & Keyword "explicit"

In C++, a single-argument constructor can be used to implicitly convert a value to an object. For example,

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> using namespace std; class Counter { private: int count; public: Counter(int c = 0) : count(c) { } int getCount() const { return count; } void setCount(int c) { count = c; } }; int main() { Counter c1; cout << c1.getCount() << endl; c1 = 9; cout << c1.getCount() << endl; }

This implicit conversion can be confusing. C++ introduces a keyword "" to disable implicit conversion. Nonetheless, you can still perform explicit conversion via type cast operator. For example,

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> using namespace std; class Counter { private: int count; public: explicit Counter(int c = 0) : count(c) { } int getCount() const { return count; } void setCount(int c) { count = c; } }; int main() { Counter c1; cout << c1.getCount() << endl; Counter c2 = 9;c1 = (Counter)9; cout << c1.getCount() << endl; }

Example: The MyComplex Class

The class is simplified from the C++ STL's template class. I strongly recommend that you study the source code of (in the header) - you can download the source code for GNU GCC.

MyComplex.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #ifndef MY_COMPLEX_H #define MY_COMPLEX_H #include <iostream> class MyComplex { private: double real, imag; public: explicit MyComplex (double real = 0, double imag = 0); MyComplex & operator+= (const MyComplex & rhs); MyComplex & operator+= (double real); MyComplex & operator++ (); const MyComplex operator++ (int dummy); bool operator== (const MyComplex & rhs) const; bool operator!= (const MyComplex & rhs) const; friend std::ostream & operator<< (std::ostream & out, const MyComplex & c); friend std::istream & operator>> (std::istream & in, MyComplex & c); friend const MyComplex operator+ (const MyComplex & lhs, const MyComplex & rhs); friend const MyComplex operator+ (double real, const MyComplex & rhs); friend const MyComplex operator+ (const MyComplex & lhs, double real); }; #endif

Program Notes:

  • I prefer to list the section before the section in the class declaration to have a quick look at the internal of the class for ease of understanding.
  • I named the data members and , that potentially crash with the function parameters. I resolves the crashes via pointer if needed. Some people suggest to name data members with a trailing underscore (e.g., , ) to distinguish from the function parameters. As private members are not expose to the users, strange names are acceptable. The C++ compiler uses leading underscore(s) to name its variables internally ( for data members, for local variables).
  • The constructor is declared . This is because a single-argument constructor can be used for implicit conversion, in this case, from to , e.g., MyComplex c = 5.5; The keyword disables implicit conversion. MyComplex c = 5.5; MyComplex c = (MyComplex)5.5; Avoid implicit conversion, as it is hard to track and maintain.
  • The constructor sets the default value for and to 0.
  • We overload the stream insertion operator to print a object on a (e.g., ). We use a non-member friend function (instead of member function) as the left operand () is not a object. We declare it as friend of the class to allow direct access of the private data members. The function return a reference of the invoking object to support cascading operation, e.g. .
  • We overload the prefix increment operator (e.g., ) and postfix increment operator (e.g., c++) as member functions. They increases the real part by 1.0. Since both prefix and postfix operators are unary, a dummy argument is assigned to postfix to distinguish it from prefix . The prefix operator returns a reference to this object, but the postfix returns a value. We shall explain this in the implementation.
  • We overload the plus operator to perform addition of two objects, a object and a . Again, we use non-member friend function as the left operand may not be a object. The shall return a new object, with no change to its operands.
  • As we overload the operator, we also have to overload operator.
  • The function's reference/pointer parameters will be declared , if we do not wish to modify the original copy. On the other hand, we omit declaration for built-in types (e.g., double) in the class declaration as they are passed by value - the original copy can never be changed.
  • We declare the return values of operator as , so that they cannot be used as lvalue. It is to prevent meaningless usages such as (most likely misspelling ).
  • We also declare the return value of as . This is to prevent , which could be interpreted as . However, as return by value a temporary object (instead of the original object), the subsequent works on the temporary object and yields incorrect output. But is acceptable as returns this object by reference.
MyComplex.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #include "MyComplex.h" MyComplex::MyComplex (double r, double i) : real(r), imag(i) { } MyComplex & MyComplex::operator+= (const MyComplex & rhs) { real += rhs.real; imag += rhs.imag; return *this; } MyComplex & MyComplex::operator+= (double value) { real += value; return *this; } MyComplex & MyComplex::operator++ () { ++real; return *this; } const MyComplex MyComplex::operator++ (int dummy) { MyComplex saved(*this); ++real; return saved; } bool MyComplex::operator== (const MyComplex & rhs) const { return (real == rhs.real && imag == rhs.imag); } bool MyComplex::operator!= (const MyComplex & rhs) const { return !(*this == rhs); } std::ostream & operator<< (std::ostream & out, const MyComplex & c) { out << '(' << c.real << ',' << c.imag << ')'; return out; } std::istream & operator>> (std::istream & in, MyComplex & c) { double inReal, inImag; char inChar; bool validInput = false; in >> inChar; if (inChar == '(') { in >> inReal >> inChar; if (inChar == ',') { in >> inImag >> inChar; if (inChar == ')') { c = MyComplex(inReal, inImag); validInput = true; } } } if (!validInput) in.setstate(std::ios_base::failbit); return in; } const MyComplex operator+ (const MyComplex & lhs, const MyComplex & rhs) { MyComplex result(lhs); result += rhs; return result; } const MyComplex operator+ (const MyComplex & lhs, double value) { MyComplex result(lhs); result += value; return result; } const MyComplex operator+ (double value, const MyComplex & rhs) { return rhs + value; }

Program Notes:

  • The prefix increments the real part, and returns this object by reference. The postfix saves this object, increments the real part, and returns the saved object by value. Postfix operation is clearly less efficient than prefix operation!
  • The operators use the operator (for academic purpose).
  • The friend functions is allow to access the private data members.
  • The overloaded stream insertion operator outputs "".
  • The overloaded stream extraction operator inputs "". It sets the of the object if the input is not valid.
TestMyComplex.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <iostream> #include <iomanip> #include "MyComplex.h" int main() { std::cout << std::fixed << std::setprecision(2); MyComplex c1(3.1, 4.2); std::cout << c1 << std::endl; MyComplex c2(3.1); std::cout << c2 << std::endl; MyComplex c3 = c1 + c2; std::cout << c3 << std::endl; c3 = c1 + 2.1; std::cout << c3 << std::endl; c3 = 2.2 + c1; std::cout << c3 << std::endl; c3 += c1; std::cout << c3 << std::endl; c3 += 2.3; std::cout << c3 << std::endl; std::cout << ++c3 << std::endl; std::cout << c3++ << std::endl; std::cout << c3 << std::endl; MyComplex c4 = (MyComplex)5.5; std::cout << c4 << std::endl; MyComplex c5; std::cout << "Enter a complex number in (real,imag): "; std::cin >> c5; if (std::cin.good()) { std::cout << c5 << std::endl; } else { std::cerr << "Invalid input" << std::endl; } return 0; }

Dynamic Memory Allocation in Object

If you dynamically allocate memory in the constructor, you need to provide your own destructor, copy constructor and assignment operator to manage the dynamically allocated memory. The defaults provided by the C++ compiler do not work for dynamic memory.

Example: MyDynamicArray

MyDynamicArray.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #ifndef MY_DYNAMIC_ARRAY_H #define MY_DYNAMIC_ARRAY_H #include <iostream> class MyDynamicArray { private: int size_; double * ptr; public: explicit MyDynamicArray (int n = 8); explicit MyDynamicArray (const MyDynamicArray & a); MyDynamicArray (const double a[], int n); ~MyDynamicArray(); const MyDynamicArray & operator= (const MyDynamicArray & rhs); bool operator== (const MyDynamicArray & rhs) const; bool operator!= (const MyDynamicArray & rhs) const; double operator[] (int index) const; double & operator[] (int index); int size() const { return size_; } friend std::ostream & operator<< (std::ostream & out, const MyDynamicArray & a); friend std::istream & operator>> (std::istream & in, MyDynamicArray & a); }; #endif

Program Notes:

  • In C++, the you cannot use the same name for a data member and a member function. As I would like to have a public function called , which is consistent with the C++ STL, I named the data member with a trailing underscore, following C++'s best practices. Take note that leading underscore(s) are used by C++ compiler for its internal variables (e.g., for data members and for local variables).
  • As we will be dynamically allocating memory in the constructor, we provide our own version of destructor, copy constructor and assignment operator to manage the dynamically allocated memory. The defaults provided by the C++ compiler do not work on dynamic memory.
  • We provide 3 constructors: a default constructor with an optional size, a copy constructor to construct an instance by copying another instance, and a construct to construct an instance by copying from a regular array.
  • We provide 2 version of indexing operators: one for read operation (e.g., ) and another capable of write operation (e.g., ). The read version is declared as a member function; whereas the write version return a reference to the element, which can be used as lvalue for assignment.
MyDynamicArray.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <stdexcept> #include "MyDynamicArray.h" MyDynamicArray::MyDynamicArray (int n) { if (n <= 0) { throw std::invalid_argument("error: size must be greater then zero"); } size_ = n; ptr = new double[size_]; for (int i = 0; i < size_; ++i) { ptr[i] = 0.0; } } MyDynamicArray::MyDynamicArray (const MyDynamicArray & a) { size_ = a.size_; ptr = new double[size_]; for (int i = 0; i < size_; ++i) { ptr[i] = a.ptr[i]; } } MyDynamicArray::MyDynamicArray (const double a[], int n) { size_ = n; ptr = new double[size_]; for (int i = 0; i < size_; ++i) { ptr[i] = a[i]; } } MyDynamicArray::~MyDynamicArray() { delete[] ptr; } const MyDynamicArray & MyDynamicArray::operator= (const MyDynamicArray & rhs) { if (this != &rhs) { if (size_ != rhs.size_) { delete [] ptr; size_ = rhs.size_; ptr = new double[size_]; } for (int i = 0; i < size_; ++i) { ptr[i] = rhs.ptr[i]; } } return *this; } bool MyDynamicArray::operator== (const MyDynamicArray & rhs) const { if (size_ != rhs.size_) return false; for (int i = 0; i < size_; ++i) { if (ptr[i] != rhs.ptr[i]) return false; } return true; } bool MyDynamicArray::operator!= (const MyDynamicArray & rhs) const { return !(*this == rhs); } double MyDynamicArray::operator[] (int index) const { if (index < 0 || index >= size_) { throw std::out_of_range("error: index out of range"); } return ptr[index]; } double & MyDynamicArray::operator[] (int index) { if (index < 0 || index >= size_) { throw std::out_of_range("error: index out of range"); } return ptr[index]; } std::ostream & operator<< (std::ostream & out, const MyDynamicArray & a) { for (int i = 0; i < a.size_; ++i) { out << a.ptr[i] << ' '; } return out; } std::istream & operator>> (std::istream & in, MyDynamicArray & a) { for (int i = 0; i < a.size_; ++i) { in >> a.ptr[i]; } return in; }

Program Notes:

  • Constructor: [TODO]
  • Copy Constructor:
  • Assignment Operator:
  • Indexing Operator:
TestMyDynamicArray.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> #include <iomanip> #include "MyDynamicArray.h" int main() { std::cout << std::fixed << std::setprecision(1) << std::boolalpha; MyDynamicArray a1(5); std::cout << a1 << std::endl; std::cout << a1.size() << std::endl; double d[3] = {1.1, 2.2, 3.3}; MyDynamicArray a2(d, 3); std::cout << a2 << std::endl; MyDynamicArray a3(a2); std::cout << a3 << std::endl; a1[2] = 8.8; std::cout << a1[2] << std::endl; a3 = a1; std::cout << a3 << std::endl; std::cout << (a1 == a3) << std::endl; std::cout << (a1 == a2) << std::endl; const int SIZE = 3; MyDynamicArray a4(SIZE); std::cout << "Enter " << SIZE << " elements: "; std::cin >> a4; if (std::cin.good()) { std::cout << a4 << std::endl; } else { std::cerr << "Invalid input" << std::endl; } return 0; }
Link to "C++ References & Resources"

The more important question in this case is what "written in such a way that it must check for self-assignment" means.

It means that a well designed assignment operator should not need to check for self-assignment. Assigning an object to itself should work correctly (i.e. have the end-effect of "doing nothing") without performing as explicit check for self-assignment.

For example, if I wanted to implemented a simplistic array class along the lines of

and came up with the following implementation of the assignment operator

that implementation would be considered "bad" since it obviously fails in case of self-assignment.

In order to "fix" it one can either add an explicit self-assignment check

or follow a "check-less" approach

The latter approach is better in a sense that it works correctly in self-assignment situations without making an explicit check for it. (This implementation is still far for perfect from the exception safety point of view, it is here to illustrate the difference between "checked" and "check-less" approaches to handling self-assignment). The later check-less implementation can be written more elegantly through the well-known copy-and-swap idiom.

This does not mean that you should avoid explicit checks for self-assignment. Such check do make sense from the performance point of view: there's no point in carrying out a long sequence of operations just to end up "doing nothing" in the end. But in a well-designed assignment operator such checks should not be necessary from the correctness point of view.

0 comments

Leave a Reply

Your email address will not be published. Required fields are marked *