< C++ .Net Early Stages 13 | Main | C++ .Net System Prog. 1 >


 

 

 

Early Stages of the C++ .Net 14

(Managed Extensions for C++)

 

 

 

In this module we start to see some of the C++ .Net implementation using VC++ 2003. Notice the new deprecated keywords used. The following are the topics for this module. The code snippets used in this module are for Visual C++ .Net 2003 and if compiled using Visual C++ .Net 2005 you need to use the /clr:oldSyntax option).

 

 

  1. Conversion Operators

  2. Managed Operators

  3. Unary Operators

  4. Binary Operators

  5. Creating and Destroying Objects

  6. Entry Points

  7. Summary

Conversion Operators

 

The Common Language Specification (CLS) defines two operators with the special names op_Explicit and op_Implicit. These operators are used for the conversion of one type to another type. op_Explicit is used when the conversion will lose data, and op_Implicit is used when no data is lost during the conversion. In most cases, in C++ code you will have to explicitly call these operators. Other CLS languages (such as C#) will call op_Explicit when a cast operator is used and op_Implicit when it is not used.

These conversion operators are always static members of a class and either takes an instance of the class as a parameter or return an instance of the class, depending on whether you are converting to another type or from another type. (Of course, conversion from another type can also be achieved with a constructor that takes a parameter of that type). These operators are interesting because you can overload them based only on the return type.

 

Managed Operators

 

As mentioned earlier, operators are used by CLS-compliant languages to implement certain language features. Languages are not required to implement nor are they required to call the operators if they exist (as can be seen with the conversion operators and C++). Operators are static members of a class, and they return the result of the operation, which is an instance of the class. Operators defined on __value types behave as you would expect: the compiler will convert the use of the C++ operator into the appropriate method call. Operators on __gc types typically have to be called directly in C++ (through a method call), but other languages might allow you to use language operators.

 

Unary Operators

 

Unary operators have a single parameter. Because operators are static, the ++ and -- are equivalent to the prefix ++ and operators that is, they return the new value. The following Table lists the unary operators available in managed C++.

 

 

Operator

Description

op_Decrement

Decrement the object, equivalent to ++.

op_Increment

Increment the object, equivalent to --.

op_LogicalNot

Used for Boolean types to reverse the value.

op_UnaryNegation

Make the item negative (+).

op_UnaryPlus

Unary +.

 

Table 2: Example of unary operators

 

Binary Operators

 

Binary operators take two parameters that are being combined with the operator. For example:

// operators.cpp

__value struct Complex

{

   int x; int y;

   Complex(int i, int j) : x(i), y(j) {}

   static Complex op_Addition(Complex lhs, Complex rhs)

   { return Complex(lhs.x + rhs.x, lhs.y + rhs.y); }

  

   String* ToString()

   {

      return String::Format(S"({0} + {1}j)", __box(x), __box(y));

   }

};

 

void main()

{

   Complex c1(1,2);

   Complex c2(2,3);

   Complex c3 = c1 + c2;

   Console::WriteLine(S"{0} + {1} = {2}",

      c1.ToString(), c2.ToString(), c3.ToString());

}

The following Table lists the binary operators available in managed C++.

 

Operator

Description

op_Addition

Add two objects.

op_Assign

Create a new object with the value of another one.

op_BitwiseAnd

Perform a bitwise AND (&) on two objects.

op_BitwiseOr

Perform a bitwise OR () on two objects.

op_Division

Divide one object by another.

op_Equality

Test the value of two objects for equality.

op_ExclusiveOr

Perform a logical XOR (^) on two objects.

op_GreaterThan

Test to see if one object is greater than another.

op_GreaterThanOrEqual

Test to see if one object is greater than or equal to another.

op_Inequality

Test the value of two objects for inequality.

op_LeftShift

Left-shift the value the specified number of places.

op_LessThan

Test to see if one object is less than another.

op_LessThanOrEqual

Test to see if one object is less than or equal to another.

op_LogicalAnd

Perform a logical AND (&&) on two objects.

op_LogicalOr

Perform a logical OR (||) on two objects.

op_Modulus

Return the remainder after dividing one object by another (%).

op_Multiply

Multiply one object by another.

op_RightShift

Right-shift the value the specified number of places.

op_Subtraction

Subtract one object from another.

 

Table 3: Common binary operators

 

Creating and Destroying Objects

 

All __gc types must be created with __gc new. All __gc types can have a C++ destructor. __value types cannot have destructors. The C++ compiler will implement this as the method with the special name of __dtor that has the special purpose of being called when the operator delete is called. If your class does not have a destructor, you cannot call delete on that type. The destructor on a __gc type does not have the same meaning as a destructor on a __nogc type, and correspondingly, calling delete on a __gc type does not mean the same as calling delete on a __nogc type.

When you create an instance of a __gc type with __gc new, the instance is created on the managed heap and your code will get a pointer to that object, which represents a reference to the object. While you use that pointer, the garbage collector knows that a reference is held to the object. When that pointer goes out of scope or if you assign zero to it, the garbage collector knows that the reference no longer exists to that object. If you copy the pointer in some way (pass the pointer to a method or do a pointer assignment), you have made another reference to the object. The lifetime of an object depends on the extant references and the amount of time until the garbage collector decides to perform garbage collection. (You can explicitly tell the garbage collector to perform garbage collection by calling System::GC::Collect). When the garbage collector determines that an object on the heap is no longer reachable from any pointer in your code, the object is a candidate for collection. In most cases, the garbage collector will merely reuse the object’s memory. However, if the object implements Object::Finalize, there is some code that the object needs to have run just before the object is freed. During the collection, the garbage collector will identify such objects and schedule them to be called on a separate thread (the finalizer thread). This thread will go through each of these objects (in no specific order) and call its Finalize method. This action delays the final demise of these objects still further.

In C++, you cannot define a Finalize method on an object; instead, you declare a destructor. When the compiler sees that your class has a destructor, it generates two methods, a public method named __dtor and the protected override of Finalize. Whatever way you declare your destructor, the compiler will always make __dtor virtual. This method is called when your code calls the delete operator or when your code calls the destructor directly for example, with this class:

__gc class Test

{

public:

   void f(){}

   ~Test(){/* dtor code */}

};

We can call the following code:

Test* t = new Test;

t->f();

delete t;

t->~Test();

t->__dtor();

t->f();

Notice that after we call delete on the pointer, the pointer is still valid. Indeed, it still remains valid after I call the destructor using C++ syntax and through the compiler-generated method. Unlike native C++, delete does not affect the pointer. In Managed C++, delete merely calls the destructor code. Of course, the code that you have in your destructor might invalidate the state of the object, so calling other methods on the object will have inherent dangers, but the object itself is still valid. The compiler places the code that you write in your destructor into the generated Finalize method. Thus, this code will be called when the object is eventually called by the finalizer thread. The __dtor method looks like this:

virtual void __dtor()

{

   System::GC::SuppressFinalize(this);

   Finalize();

}

This code calls the Finalize method (which contains the code that you put in the destructor). When this method is called, it means that your cleanup code has already been called, so you do not want the garbage collector to do this again. This is why __dtor calls SuppressFinalize. Because the object still exists after the destructor has been called (and indeed, unless you assign the pointer to zero, a reference will still exist to the object), you can still access the object. If the object relies on resources that might have been released in the destructor, you will need to reinitialize these resources before object methods can be called, but because SuppressFinalize has been called, the garbage collector will not now call the Finalize method. The solution to this issue is to call ReRegisterForFinalize in the method that reinitializes these resources. It is a good idea not to allow objects to be used like this. Indeed, objects that hold onto resources should implement an interface called IDisposable, and once disposed, such an object should throw an ObjectDisposedException. If your class has a base class that also has a Finalize method (for example, if it was written in C++ with a destructor), the destructor for your class will call the base class Finalize after your class’s Finalize has been called.

This behavior of the destructor, prolonging the lifetime of your objects, is a problem, and you should try to avoid it where possible. To do so often requires rethinking the problem to avoid holding onto resources for the lifetime of the object. Instead, retain the resources only as long as you need them. Another trick that you can employ that destructors on unmanaged classes are called just as you will expect them to be. So you can create temporary objects on the stack, and when the object is destroyed, its destructor is called. You can write unmanaged classes that hold managed pointers as data members obtained in the constructor and released in the destructor. Like native C++, when a __gc class is created, an appropriate constructor is called on the base class. (This will be the default constructor, but you can specify another constructor.) The default constructor on System::Object will be called before any code that you specify in your class’s constructor is called. If your class has virtual methods and a base class constructor calls these methods, there will be no problems even though officially the object has not been called yet. The reason is that the object’s members will be initialized to zero before any constructors are called, so they will have valid values. Indeed, in contrast to native C++, the vtable for an object is created before a constructor is called, so it is safe to call virtual methods.

 

The Entry Points

 

When you compile your code, the compiler will assume that you will use the C-Run-Time (CRT), in which case the entry point will be mainCRTStartup. This function calls your main function, passing the command-line arguments. The command-line arguments will be available through a char* pointer or wchar_t* pointer array depending on whether the code was compiled with the UNICODE symbol defined. These parameters are always unmanaged, so if you want to use them in your code, you will have to convert these strings to managed strings. (System::String has two constructors that take a char* pointer or a wchar_t* pointer parameter). Because main() is the function that you are used to using in traditional C programs, you can expect to get the same parameters. For example, the following are all valid signatures for the entry point of a managed console process:

  1. void main();

  2. int main();

  3. int main(int argc, char* argv[]);

  4. int wmain(int argc, wchar_t* argv[]);

  5. int main(int argc, char* argv[], char* envp[]);

  6. int wmain(int argc, wchar_t* argv[], wchar_t* envp[]);

Also, you can link with setargv.obj, and the command-line arguments will be treated as file specifications with the wildcards * and ? expanded. If you will not use the CRT and do not have any global native C++ objects, you do not need to initialize the CRT and you can make main your entry point.

cl /clr mycode.cpp /link /entry:main

The code that loads the application will assume that the main function takes no parameters, so you do not have access to the traditional parameters of the C main function: argc and argv. If you want to get the command line, you can call Environment::CommandLine to get the command line as one string, and Environment::GetCommandLineArgs to get an array of the command-line arguments. In both cases, the first argument will be the command name that you used to start the process. You can call Environment::GetEnvironmentVariables to get a dictionary (a name-value associative container) of the environment variables. Command-line processes can return an integer to the operating system that is often used as an error level. If you forget to return a value from the entry point, the compiler will automatically return zero. We don’t recommend that you use this facility. You can return a value back from your managed entry point, or you can call the managed Environment::Exit with the error level.

Your assembly can be a GUI application, which means that the PE file must be marked as such so that a console is not created when it is run, and your application must have a WinMain entry point. If you add WinMain to your code, the compiler will use this function as the entry point and the linker will ensure that it uses the /SUBSYSTEM:WINDOWS switch. If the assembly is a library, your code does not need an entry point, the assembly is loaded by the .NET Fusion technology and not the LoadLibraryEx function. In fact, if your library assembly has a DllMain it will be called but only when the assembly is first loaded, when it is passed a value of DLL_PROCESS_ATTACH. So you do not need a DllMain. By default, you’ll get a _DllMainCRTStartup function to initialize the CRT. If you do not need the CRT, you can use the /noentry linker switch to remove the _DllMainCRTStartup function.

 

Summary

 

.NET is touted as having a common language runtime. In fact, the runtime will execute only one language, Microsoft intermediate language, but the assemblies that contain the MSIL can be created by any .NET language and any .NET language can use types in .NET assemblies. The Managed Extensions for C++ allow you to use C++ to write .NET code, code that implements and uses .NET types. Managed C++ extends the language with new keywords that allow you to specify that a class is a .NET class and allow you to add metadata to the class and its members. Code that does not have these new keywords will be native C++, and instances of these types will not be managed by the .NET garbage collector, although in most cases, their code will be compiled to MSIL. Managed C++ gives you all the .NET features that are available to other .NET languages, but it has all the power of a language that has always been regarded as the language of choice for power programming. The significant point about C++ is that it allows you to compile both managed and unmanaged types and to use native code all in the same project.

 

 

 

 

 

 

Part 1 | Part 2 | Part 3 | Part 4

 


 

< C++ .Net Early Stages 13 | Main | C++ .Net System Prog. 1 >