Overloading Operators in Managed Types


Overloading operators in managed types is quite different from how you overload them in traditional C++. The CLS defines the list of arithmetic, logical, and bitwise operators that .NET languages can support, and compiler vendors use the language’s native operators to implement these functions. So, when you override an operator in a managed C++ type, you don’t override the C++ operator, but instead, you override the underlying CLS functionality.

Note

If you’re currently a C++ programmer, be aware that the compiler won’t let you implement normal C++ operator overloads in managed types. You have to use the overloading mechanism that I’ll describe.

Overloading Value Types

Let’s start by adding operator overloading to value types and then move on to reference types. You already know that value types are the types most likely to need operator overloading.

Overloading Arithmetic Operators

In this exercise, you will see how to implement operators in a value type. The exercise also introduces many of the techniques you’ll need to use when adding operator overloading to your own types.

  1. Start Microsoft Visual Studio .NET, and open a new Microsoft Visual C++ Console Application (.NET) project named Overload.

  2. At the top of the Overload.cpp file, immediately under the using namespace System; line, add the following struct definition:

    // The Dbl struct definition __value struct Dbl { double val; public: Dbl(double v) { val = v; } double getVal() { return val; } };

    This simple Dbl struct is the one you’ll use throughout these exercises. It simply wraps a double and then provides a constructor for creating and initializing Dbl objects and a get function for accessing the data member. As you might remember from Chapter 9, the keyword __value makes Dbl a .NET value type rather than a traditional C++ struct.

  3. Create three Dbl objects in your _tmain function.

    Dbl d1(10.0); Dbl d2(20.0); Dbl d3(0.0);
  4. Add a call to Console::WriteLine to print out the value of d3:

    Console::Write(S"Value of d3 is "); Console::WriteLine(d3.getVal());

    Remember that you have to use the dot operator to reference members of a managed object.

  5. Try adding d1 and d2 and assigning the result to d3. Insert this line into your code immediately before the call to Console::Write:

    d3 = d1 + d2;

    When you try this, you’ll find that it doesn’t work; the compiler gives you a C2676 error.

    click to expand

    The compiler is telling you that it doesn’t have a + operator to use that works with Dbl objects, so it can’t perform the operation.

  6. Implement the + operator for the struct by adding the following code to the struct definition, immediately after the getVal function:

    // The addition operator for the Dbl struct static Dbl op_Addition(Dbl lhs, Dbl rhs) { Dbl result(lhs.val + rhs.val); return result; }

    Let’s analyze this function. The keyword static, which you met in Chapter 6, tells you that this function belongs to the Dbl struct as a whole, rather than to any one instance of the struct. The addition operation is implemented by the op_Addition function, which is one of the overloadable functions defined in the CLS. The following table lists the most commonly used operators supported by the CLS, together with their C++ equivalents.

    Operation

    C++ Operator

    CLS Function

    Decrement

    --

    op_Decrement

    Increment

    ++

    op_Increment

    Unary minus

    - (unary)

    op_UnaryNegation

    Unary plus

    + (unary)

    op_UnaryPlus

    Addition

    + (binary)

    op_Addition

    Subtraction

    - (binary)

    op_Subtraction

    Multiplication

    *

    op_Multiply

    Division

    /

    op_Division

    Modulus

    %

    op_Modulus

    Assignment

    =

    op_Assign

    Equality

    ==

    op_Equality

    Inequality

    !=

    op_Inequality

    Less than

    <

    op_LessThan

    Less than or equal to

    <=

    op_LessThanOrEqual

    Greater than

    >

    op_GreaterThan

    Greater than or equal to

    >=

    op_GreaterThanOrEqual

    Logical AND

    &&

    op_LogicalAnd

    Logical OR

    ||

    op_LogicalOr

    Logical NOT

    !

    op_LogicalNot

    Left-shift

    <<

    op_LeftShift

    Right-shift

    >>

    op_RightShift

    Bitwise AND

    &

    op_BitwiseAnd

    Bitwise OR

    |

    op_BitwiseOr

    Exclusive OR

    ^

    op_ExclusiveOr

    To overload an operator, you pick the equivalent function from the list and implement it in your class. The arguments a function takes and what it returns depend on the function. In this case, we’re implementing the binary addition operator, so the arguments are going to be two Dbl values, and the return type will also be a Dbl value.

    You can see how the function works. The result of adding two Dbl values has to be a third Dbl, so you create a third one, initialize it with the contents of the two operands, and return it.

    start sidebar
    Operator Overloading in Unmanaged C++

    If you need to provide overloaded operators in traditional, unmanaged C++, you’ll find that operator overloading is done rather differently. Operator overloads use the operator keyword to name an overload function, so the addition operator is implemented by a function named operator+, the equality operator by one named operator==, the increment operator by one named operator++, and so on. These functions can either be member functions of the type or global functions that aren’t a member of any type.

    There’s a lot more to operator overloading in unmanaged C++. If you need to know more, consult a book on C++ programming. Bjarne Stroustrup’s The C++ Programming Language (Addison-Wesley, 1997) is the classic C++ text.

    end sidebar

  7. Try compiling your program again.

    You should find that the compilation is successful this time because the compiler recognizes that you have implemented the + operator. If you run the program, you should get the answer 30.0 printed out because of the following two lines:

    Console::Write(S"Value of d3 is"); Console::WriteLine(d3.getVal());

    If your type implements an operator, the compiler maps C++ operators (such as +) onto the equivalent op_ function for you. You can also call the operator function explicitly, although there’s generally little reason why you’d want to do so. So, you could replace the addition line, as follows:

    // d3 = d1 + d2; d3 = Dbl::op_Addition(d1, d2);

    A static method is accessed by using the type name followed by the :: operator and the name of the function.

    Now that you’ve seen how to implement the addition operator, it will be simple for you to implement the other arithmetic operators for the struct (subtraction, division, multiplication, and modulus).

Overloading Operator Functions

You can also overload the operator functions themselves. For example, suppose you wanted to handle the following addition operations:

// Add two Dbl’s d3 = d1 + d2; // Add a Dbl and an int d3 = d1 + 5; // Add an int and a Dbl d3 = 5 + d2;

You can add overrides of the op_Addition function to handle this task. The following short exercise shows you how.

  1. Continue using the project from the previous exercise.

  2. Find the op_Addition function in the Dbl struct. Add the following two variations after it:

    // Adding a Dbl and an int static Dbl op_Addition(Dbl lhs, int rhs) { Dbl result(lhs.val + rhs); return result; } // Adding an int and a Dbl static Dbl op_Addition(int lhs, Dbl rhs) { Dbl result(lhs + rhs.val); return result; }
  3. Add two more additions to your _tmain function to test the new operator functions, plus some calls to Console::WriteLine to verify that the additions have worked correctly.

    // Add two Dbl’s d3 = d1 + d2; Console::Write(S"Value of d3 is "); Console::WriteLine(d3.getVal()); // Add a Dbl and an int d3 = d1 + 5; Console::Write(S"Value of d3 is now "); Console::WriteLine(d3.getVal()); // Add an int and a Dbl d3 = 5 + d2; Console::Write(S"Value of d3 is now "); Console::WriteLine(d3.getVal());
  4. Now for the interesting step: remove (or comment out) those last two operator overloads you added in Step 1, and then recompile the code and run it.

    You should find that it works exactly the same as it did with the operators left in. So what’s going on? You can find out by using the debugger.

  5. Place a breakpoint in the code by clicking in the gray margin to the left of the code, next to the d3 = d1 + 5 line.

    You should see a red dot in the margin, showing that a breakpoint has been set.

    click to expand

  6. Run the program by pressing the F5 key. The program will start up and then stop at the breakpoint. Now press the F11 key to step into the next function to be executed. You might expect this function to be op_Addition, but you’d be wrong because you step into the Dbl constructor.

  7. Press F11 again, and you’re finished with the constructor and back to the d3 = d1 + 5 line. Press it once more, and you’re in op_Addition, where you might expect to be.

    Why did you get an extra call to the Dbl constructor? Because the compiler saw an integer in the code, but it only has an op_Addition function that takes two Dbl values. However, because the Dbl struct has a constructor that takes a double, the compiler has cast the integer as a double and then used the constructor to create a temporary Dbl object to pass to the function.

    When you’re in the op_Addition function, if you examine the arguments in the Locals window, you’ll see that they are both of type Dbl and that the rhs argument has the value 5.0.

You can see that, provided your types implement the correct constructors, you can sometimes use one operator overload to perform several different conversions.

Implementing Logical Operators and Equality

We’ve now dealt with the arithmetic operators, so let’s continue by considering the logical and comparison operators. As you’ve already seen, C++ provides a set of comparison operators, and they’re summarized in the following table.

Operator

Description

==

Equality

!=

Inequality

>

Greater than

>=

Greater than or equal to

<

Less than

<=

Less than or equal to

Implementing these operators is quite simple and follows the model of the addition operator in the previous exercises. Here’s how to implement the equality operator (==):

  1. Using the same project as in the previous exercises, find the op_Addition function in your code and add the following function after it:

    // The equality operator for the Dbl struct static bool op_Equality(Dbl lhs, Dbl rhs) { return lhs.val == rhs.val; }

    The function follows the same pattern as those you implemented for the arithmetic operators. It is a static member of the Dbl type, but this time, it returns a Boolean, just as you’d expect a logical operator to do, and it makes its decision by comparing the inner structure of its two operands.

    start sidebar
    What Is Equality?

    Deciding whether to implement == and != depends on the type you’re writing, and it might not be a simple decision. For some classes, the choice is fairly obvious: take a Point type, which has x and y members. In this case, == would compare the x and y members of two Points and return true if they’re the same.

    What about a Currency class that has a value and a currency type? You might say that two Currency objects are the same if both the value and the currency type are identical. Likewise, you might say that the two objects are identical if their values are the same when converted to some underlying base currency, such as dollars or Euros. Both view points are equally valid; it’s up to you to choose one and document it.

    Additionally, there might be classes for which any notion of equality is artificial. Consider a BankAccount class: what would equality mean? Two account objects can’t be identical because they have different, unique account numbers. You might choose some criteria that you could test for equality, or you might decide that equality doesn’t apply to BankAccount objects.

    As a final point, you should be aware that testing for equality can pose problems for floating-point values. Some values can require more decimal places than the float and double types support, so there might be rounding errors due to rounding the final decimal place up or down. It’s quite possible that rounding errors could mean that two identical values will apparently be unequal.

    One way around this problem is to define a class, such as Dbl, that makes allowances for this situation by testing whether the difference between two values falls within a given tolerance. To provide a more realistic test, you can code the equality operator for Dbl like this:

    // A better equality operator for the Dbl struct static bool op_Equality(Dbl lhs, Dbl rhs) { if (Math::Abs(lhs.val - rhs.val) < 0.00001) return true; else return false; }

    The Math::Abs method is a static member of the Math class that returns an absolute value.

    end sidebar

  2. Add some test code to the _tmain function to test the new operator.

    if (d1 == d2) Console::WriteLine(S"d1 and d2 are equal"); else Console::WriteLine(S"d1 and d2 are not equal");

    As you’d expect, d1 and d2 aren’t equal, so if you compile and run your program, you see the second message.

  3. You can take a shortcut when you implement the inequality operator (!=) by making use of the fact that its result is the opposite of whatever == would return for the same operands. So, you can implement the inequality operator in terms of the equality operator. Add the following code after the overload for op_Equality:

    // The inequality operator for the Dbl struct static bool op_Inequality(Dbl lhs, Dbl rhs) { return !(lhs == rhs); // calls op_Equality() }

    The function compares its two arguments using the == operator, which causes the op_Equality function to be called. The op_Inequality function then applies the not operator (!) to turn a true result into false, and vice versa.

    In this case, using the shortcut doesn’t save us any code, but if op_Equality ended up checking a lot of data members, this shortcut could be worthwhile. It will also help if the structure of the class changes because you’ll only have to change the implementation of op_Equality.

    The other logical operators (<, <=, >, and >=) can be overloaded in a similar manner, and you can make use of the same shortcuts when implementing pairs of operators.

Implementing Equals

You’ve already learned that all types in .NET ultimately inherit from the Object class. This class provides several functions that all .NET types inherit, and one in particular is relevant to our discussion of the == and != operators: Equals.

The Object::Equals method is intended to let types provide a way of comparing content, as opposed to comparing references. This is the same task that you’ve performed by implementing the == operator. However, one of the great attractions of .NET is that managed types can be accessed seamlessly from other languages, and other languages might well want to use Equals to test for equality.

The following exercise shows you how to implement Equals for the Dbl struct.

  1. Using the same project as in the previous exercises, find the op_Inequality function in your code and add the following function after it:

    // The equality operator for the Dbl struct bool Equals(Object* pOther) { Dbl* s = dynamic_cast<Dbl*>(pOther); if (s == 0) return false; return s->val == val; }

    This operator function is more complex than others in this chapter, so let’s look at it line by line. The first line declares the Equals function to take an Object* as its single argument and return a bool. It’s important that you declare it exactly this way; otherwise, it won’t be an override for the virtual function inherited from the Object base class.

    The use of Object* for the argument type means that you can pass a pointer to any managed type into the function, so the first thing you need to do is to make sure that it is actually a Dbl* that has been passed in. You run this test by using a dynamic cast. The dynamic cast is a mechanism introduced by C++ that allows you to perform a cast at run time. Here the cast is from Object* to Dbl*. If the cast fails—if the pointer passed in isn’t a Dbl*—the result will be a null pointer. The function checks for this possibility and returns false if it is the case. If the dynamic cast works, you know that you have a Dbl*; therefore, you can check its content to see if it is the same as that of the object.

  2. Calling the Equals method from code is more complicated than using the operators we’ve already met because of the need to pass in an Object pointer. Add the following code to the _tmain function after your test of the == operator:

    if (d1.Equals(__box(d2))) Console::WriteLine(S"d1 and d2 are equal"); else Console::WriteLine(S"d1 and d2 are not equal");

    This code calls the Equals function on d1 to see whether d1 is equal to d2. Equals needs a pointer to an Object, so you need to somehow get an Object* representing d2. You do so by using the keyword __box.

You learned in Chapter 9 how value types are as efficient as built-in types, but they can also be used as reference types when the need arises. Treating a value type as a reference type is done by boxing it. Boxing is discussed in more detail in Chapter 25, but it basically involves wrapping an object around a value type. The object wrapper is a reference type and acts as a box to contain the value.

This process might seem complicated, but you’re really providing Equals for other .NET languages to use. In managed C++ code, you’d usually use == instead, without worrying about boxing.

Tip

If you implement Equals for a type, it’s strongly recommended that you implement the == operator.

Implementing Assignment

The op_Assign function provides a way to overload the assignment operator (=). Why would you want to do this? Suppose you have a class that contains a date and a time, and you want to make sure that these member variables hold the date and the time when the object is created or assigned to. If you don’t implement op_Assign, the existing values will be copied straight from the right side over to the left side. By implementing the operator, you can customize the copy operation and ensure that the date and the time are updated when the assignment is performed.

The following short exercise shows you how to implement op_Assign for the Dbl struct.

  1. Using the same project as in the previous exercises, add three data members to represent the time. Add the following line after the declaration of the val variable:

    double hr, min, sec; // creation or assignment time

    In addition, alter the constructor so that the time fields are initialized to the current time. The new constructor should read:

    Dbl(double v) { val = v; // Create new date and time fields DateTime dt = DateTime::Now; hr = dt.get_Hour(); min = dt.get_Minute(); sec = dt.get_Second(); }

    The constructor uses a System::DateTime object to hold the current time; this object is initialized by a call to the static DateTime::Now property. The individual fields are then saved into the appropriate members of the object.

  2. Now add the assign function to the Dbl struct.

    // The assignment operator for the Dbl struct static Dbl op_Assign(Dbl& lhs, const Dbl& rhs) { // Copy over the value lhs.val = rhs.val; // Create new date and time fields DateTime dt = DateTime::Now; lhs.hr = dt.get_Hour(); lhs.min = dt.get_Minute(); lhs.sec = dt.get_Second(); return lhs; }

    You’ll notice several things about this function. First is the fact that it uses references for the arguments. Passing values by reference does not cause a copy to be made. Instead, the name you use in the function is a link back to the argument that was passed in. Passing in values by using references is essential when you think about how assignment has to work. Let’s say you code a line like this:

    obj2 = obj1;

    You want obj2 to contain the values from obj1 when the function returns. If you pass the arguments by value—without the &—any changes to lhs will be lost because this local variable ceases to exist at the end of the function. Making the arguments to the function references provides a link back to the value that was passed in, so changes made to lhs will be persistent. The rhs argument is made a const reference because you don’t want to make changes to the right side and the keyword const ensures that you can’t.

    The val member is copied over, and then a new set of time members is generated so that the object will reflect the time it was assigned, not the original time it was created.

  3. To test the changes, you have to arrange some sort of time delay between creating the object and assigning to it so that you can see that the times are different. The easiest way to do this is to use the static Sleep method from the System::Thread class, so add the following code to your _tmain method:

    // Create two objects Dbl one(0.0); Dbl two(10.0); // Sleep for five seconds System::Threading::Thread::Sleep(5000); // Do an assignment one = two;
  4. The best way to see the result is to run the code under the debugger. Place a breakpoint in the code on the line where the one object is created by clicking in the gray margin to the left of the code. You’ll see a red dot appear, indicating an active breakpoint.

  5. Run the program by pressing F5, and the code runs until the breakpoint is reached. Press F10 to single-step over the creation of the one object. Now look in the Locals window, and click the plus sign (+) to the left of one to expand it. You see the time values assigned to the object.

    click to expand

  6. Now press F10 to single-step through the code, past the call to Sleep. When you pass the assignment, you see that the data members in one change; val takes on the value of the val member from two, while the time reflects the time when the assignment was done.

Implementing Increment and Decrement

As a final example, the exercise in this section shows you how to overload the increment and decrement operators (++ and --). As you saw in Chapter 3, the built-in ++ and -- operators are used to increment and decrement the values of numeric variables. You can overload them for your own numeric types, but you can also overload these operators to provide other functionality. For example, suppose you had a Date type that holds day, month, and year values; you could implement ++ and -- to add or subtract one day from the current date, adjusting the month and year as appropriate.

The following exercise shows you how to implement the ++ operator for the Dbl type that you’ve been working with in this chapter.

  1. Using the same project as in the previous exercises, find the Equals function in your code and add the following code immediately after it:

    // The increment operator for the Dbl struct static Dbl op_Increment(Dbl d) { d.val += 1; return d; } 

    The ++ operator is implemented by the op_Increment function; the
    -- operator is implemented by op_Decrement. The single argument represents the value to be incremented, so 1 gets added to its value and then a copy is returned.

  2. Use the operator in code like this:

    // Create a Dbl, and increment it Dbl d5(1.0); Console::Write(S"Initial value of d5 is "); Console::WriteLine(d5.getVal()); // Increment it d5++; Console::Write(S"Value of d5 is now "); Console::WriteLine(d5.getVal());

    The answer 2 should print out, representing the new value of d5. Note that even though the function returns a Dbl, the return value isn’t being used in this example and you’re executing the function only to get its side effect of incrementing the value.

Once you’ve implemented the increment operator, you’ll find it simple to implement decrement as well.

Note

You saw in Chapter 3 that there are two forms of the increment and decrement operators: the prefix form that goes before the operand (++x) and the postfix form that follows the operand (x++). When overloading managed types, it isn’t possible to distinguish between prefix and postfix operators, so the same op_Increment or op_Decrement function is called for both. In traditional C++ operator overloading, you can overload both prefix and postfix versions separately.

Overloading Reference Types

You can overload operators for reference types in the same way that you do for value types, but you need to be aware of the issues detailed in this section.

Implementing Overloaded Operators for Reference Types

You already know that reference types are accessed using pointers, which means that the arguments to operator functions for reference types always have to be pointers. So, if you were to implement op_Addition for a reference type, you would have to code it something like this:

// The addition operator for a reference type static MyRef* op_Addition(MyRef* pLhs, MyRef* pRhs) { MyRef* result = new MyRef(pLhs->val + pRhs->val); return result; }

Calling Overloaded Operators for Reference Types

You can’t call overloaded operators implicitly for reference types, so you have to call the overload function directly, like this:

// This doesn’t work MyRef* r3 = one + two; // This does MyRef* r3 = MyRef::op_Addition(one, two);
Note

This limitation might be removed in later versions of Visual C++ .NET.




Microsoft Visual C++  .NET(c) Step by Step
Microsoft Visual C++ .NET(c) Step by Step
ISBN: 735615675
EAN: N/A
Year: 2003
Pages: 208

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net