|
Introduction
You’ve already seen how to construct classes and structs, provide member functions in your types, and use these functions in programs. In this module, you’re going to find out about a special category of member functions called overloaded operator functions, which allow you to add extra functionality so that your types can be used more naturally and intuitively. If you’ve met operator overloading in C++ before, you need to be aware that overloading is handled completely differently in managed C++ than in traditional C++ though the syntax-wise may be similar. In fact, managed C++ types aren’t allowed to implement traditional C++ overloaded operators, so you’ll need to pay close attention to this module to find out how operator overloading is now done.
What Is Operator Overloading?
You already met the operators provided by the C++ language before. The problem is that those operators work only with the built-in types, and you’re starting to use classes and structs to define your own data types. This means that if you want to do an addition operation or a comparison operation on types that you’ve created, you can’t use the + and == operators because the compiler doesn’t know how to apply them to your types. Operator overloading is a C++ feature that lets you define operators to work with your types, that is a user defined type which can often lead to a more natural style of programming, so instead of writing this:
object3 = object1.Add(object2);
you can write this:
object3 = object1 + object2;
What Types Need Overloaded Operators?
In general, overloaded operators are needed by classes that wrap simple values. Types can be split into three broad classifications, as shown in the following table.
Classification | Defining Characteristics | Examples |
Values | Values wrap data; if two objects contain the same data, those objects are identical. | String, Matrix, Date, and Time |
Services | Services have little or no state data. They provide services through their member functions. | CreditCardCheck and AddressLookup |
Entities | Entities have an identity that is unique for each object. | BankAccount (identified by account number) and Person (identified by Social Security number) |
Table 1 |
Values are the classes for which you’ll most often find yourself implementing overloaded operators. You can imagine wanting to implement +, >, ==, and other operators for types such as Date and String, but it’s harder to see when you might want them for the other classifications. Service types, which have little or no state, don’t tend to need operators: what would comparing two AddressLookup objects mean? Entity types might have some operators, but their meaning might not be intuitive. You could use == to check two BankAccounts for equality, but what would that mean? There’s more on equality later on in this module; let’s move on to see how operator overloading works.
What Can You Overload?
You learned about the rich set of operators that C++ supports. You can overload many of these, but there are some restrictions. Traditional C++ won’t let you overload several of the more esoteric operators, such as sizeof() and the member-of dot operator. Managed C++ extends the list and adds a number of other C++ operators that can’t be overloaded, including ->, (), and []. The main reason for this restriction is that the Common Language Specification (CLS) is designed for use across languages, and as such, it will support a set of operators that are useful to all .NET languages, rather than support C++-specific operators. You’ll see later exactly which operators .NET lets you overload.
Rules of Overloading
Several rules apply when overloading operators. The problem is that you can implement operators to mean whatever you like, so some rules are needed to impose some limits and to prevent giving the compiler an impossible job.
You cannot define any new operators. Even if you think that %% would make a neat new operator, you can’t add it.
You can’t change the number of operands taken by an operator. You might think it would be really useful to create a unary / operator, but the division operator always has to have two operands.
You can’t change the precedence or associativity of operators. So, * will always take precedence over +, regardless of what they are actually implemented to mean for a type, and (for example) the left operand of a * will always be evaluated before the right operand.
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. 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 we’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 C++/Studio .NET, and create a new CLR Console Application 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 previous modules, the keyword value makes Dbl a .NET value type rather than a traditional C++ struct.
3. Create or instantiate three Dbl objects in your main() 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("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;
6. Build your project. When you try this, you’ll find that it doesn’t work; the compiler gives you a C2676 error.
1>------ Build started: Project: Overload, Configuration: Debug Win32 ------
1>Compiling...
1>stdafx.cpp
1>Compiling...
1>AssemblyInfo.cpp
1>Overload.cpp
1>.\Overload.cpp(22) : error C2676: binary '+' : 'Dbl' does not define this operator or a conversion to a type acceptable to the predefined operator
1>Generating Code...
1>Build log was saved at "file://f:\vc2005project\Overload\Overload\Debug\BuildLog.htm"
1>Overload - 1 error(s), 0 warning(s)
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========
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.
7. Implement the + operator for the struct by adding the following code to the struct definition, immediately after the getVal function:
static Dbl operator+(Dbl lhs, Dbl rhs)
{
Dbl result(lhs.val + rhs.val);
return result;
}
|
Let’s analyze this function. The keyword static, 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 operator+ function, which is one of the overloadable functions defined in the new C++. The operator keyword declares a function specifying what operator-symbol means when applied to instances of a class. This gives the operator more than one meaning, or "overloads" it. The compiler distinguishes between the different meanings of an operator by examining the types of its operands. The general syntax for operator overloading is:
type operator operator-symbol ( parameter-list )
The name of an overloaded operator is operatorx, where x is the operator that can be overloaded (refer to the list at the end of this module or column C++ Operator in the following Table). For example, to overload the addition operator, you define a function called operator+. Similarly, to overload the addition/assignment operator, +=, define a function called operator+=. The following table lists the most commonly used operators supported by the CLS, together with their C++ equivalents. So the use of the new C++ operator keyword for operator overloading reverts back to the traditional C++ when the CLS became a standard. The Managed Extension for C++ uses the CLS functions as shown in the following Table.
Operation | C++ Operator | CLS Functions/Methods |
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 |
Table 2 |
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.