Multifile Libraries


So far, in every example, you have used only one file as the source of an application. For small example or demonstration programs this might be okay, but for more complex applications, using multiple source files to break up an application to make it more readable is a much better approach.

Breaking up the source code of an application into its possible many parts can be done in any number of different ways. One of the most common approaches is to break off the source into groups of common functionality, better known as libraries. Libraries are a powerful means of breaking up an application because they are more conducive to code reuse, and only at the cost of some minor up-front design work.

The first thing that you will confront when building multifile libraries is that all types need to be declared before they are used in Managed C++. This is not a problem in a single file, as all you have to do is place the declaration of the type before it is used. The only time you might have problems with this is in the case of recursive types that call themselves before they are declared. To solve this, you have prototyping.

With multifile libraries, you run into the problem of how to access a type that is declared in a different file. You could create a whole bunch of prototypes and copy the class definition that you need in every file that uses them, but then you are going to be living in maintenance hell for the lifetime of the library. A better solution is to use header files to hold all these definitions and then #include them at the start of any source file that uses these definitions.

Basically, almost all Managed C++ libraries (and applications, for that matter) should be broken up into two types of files: header files and source files. A header file is made up of the code needed to describe the types that are used, and a source file is made up of all the code that implements these types.

click to expand

With this split, it is a simple thing to place all needed definitions of types by a source file at its top. You discovered earlier that it is a simple matter to place all the declarations in a header file and then insert the contents of the header into the main source code using the #include directive. Coding this way also ensures that all types will be declared before they are used, just as they need to be, by Managed C++.

Okay, you know that you can split source code into two parts, and you know how to actually include the definition part of the source. Let's examine the two parts in more detail.

Header Files

Header files look very similar to all the examples that you have seen in this book so far. Instead of ending in .cpp, they usually end in .h, but that is not mandatory and they can end with anything. The only real difference between what you have seen in the previous chapter's header file is that they only contained the definition portion of functions, member properties, and member methods. Basically, header files are made up of function prototypes and class definitions. In fact, it is legal to place the implementation of a class within a header file.

Here is an example of a header file:

 //square.h __gc class Square {    Int32 Dims; public:     Square ( Int32 d);     Int32 Area(); }; 

Notice, the only difference between this file and what you have seen previously is that there is no main() function, and the constructor, Square(), and the member method, Area(), are only declared and have no implementation. You could, in fact, have implemented both the constructor and the member method and the header file still would have been valid because classes in Managed C++ are just definitions. What you can't include in header files are function implementations, for example, the main() function. What you can include are only function prototypes.

Source Files

You have seen source files previously in this book. They are Managed C++ files that end with .cpp. With traditional C++ source files, the definition is not found in the source file, unlike all the examples you have seen thus far. Instead, they contain only the implementation of the definitions specified in the header file.

The syntax for implementing member methods in a separate source file from their definitions is similar to that of the function, which was covered in Chapter 2, except that the member method is prefixed with the name of the class it is implementing and the scope resolution (::) operator.

The following example shows the source file for the square.h header file listed previously. Its structure is very typical of all Managed C++ source files. It starts off with the standard #using declarative found in all Managed C++ source files, which is then followed by the using namespace System; statement. Next comes the include statement for the header file, which this source file will be defining, and then, finally, the actual implementations of all the unimplemented member methods.

 // square.cpp #using <mscorlib.dll> using namespace System; #include "square.h" Square::Square ( Int32 d) {     Dims = d; } Int32 Square::Area() {     return Dims * Dims; } 

Namespaces

Adding a namespace to a library is optional but highly recommended. Remember that all identifiers have to be unique in Managed C++, at least within their own scope. When you develop code on your own, keeping identifiers unique should not be a problem. With careful coordination and a detailed naming convention, a small group of programmers can keep all their identifiers unique. However, with the addition of third-party source code, unique identifiers become increasingly harder to maintain. That is, unless namespaces are used.

Namespaces basically create a local-scope declarative region for types. In other words, namespaces allow a programmer to group code under a unique name. Thus, with the use of a namespace, it is possible for the programmer to create all types with any names she wants and be secure in the knowledge that the types will be unique within the application if they are placed within a uniquely identified namespace.

Note

Chapter 2 covers namespaces.

The basic syntax of a namespace is simply this:

 namespace name {      // all types to be defined within the namespace } 

Thus, if you want a namespace called Test to provide local scope to the Square class defined previously, you would simply code it like this:

 namespace Test {     public __gc class Square     {         Int32 Dims;     public:         Square ( Int32 d);         Int32 Area();     }; } 

Those of you with a traditional C++ background may have noticed the additional keyword public placed in front of the class declaration. Managed C++ handles namespaces differently from traditional C++. Types within a namespace have private access. Thus, to make the class accessible outside the namespace, it has to be declared public. In traditional C++, all types are public within a namespace.

Personally, I don't like the new syntax, as it is inconsistent with C++. It should be public: (be careful, this is invalid syntactically), as it is in classes and structures. This syntax resembles C# and Java instead.

Caution

If you fail to make any of the classes within the namespace public, then the namespace will not be accessible and will generate an error when you attempt to use the using statement for the namespace.

The syntax to implement a member method within a namespace does not change much. Simply add the namespace's name in front of the class name, delimited by the scope resolution (::) operator.

 #using <mscorlib.dll> using namespace System; #include "square.h" Test::Square::Square ( Int32 d) {     Dims = d; } Int32 Test::Square::Area() {     return Dims * Dims; } 

Building Assemblies From Multifile Libraries

I don't cover assemblies until Chapter 17, so let's not get bogged down with the details of what an assembly really is until then. For now, think of an assembly as a specially formatted .dll or .exe file that is executed by the CLR.

A key feature that you need to know about assemblies is that they're self-describing. What does that mean to a Managed C++ programmer? Simply put, you don't need header files to use the types placed within an assembly. Or, in other words, all those header files you meticulously created when you built your library are no longer needed once you finish creating your assembly. This is a major change from traditional C++.

Note

Header files are not needed with assemblies!

Building Multifile Library Assemblies using the Traditional Method

You will look at how to actually access an assembly later in this chapter. But the fact that headers are not needed can play a big role in how you code your libraries. The traditional C++ way of creating a library, either static or dynamic, is to create a set of header files to describe all the functionality found within the library. Then, in separate source files, implement all the functionality defined by these header files. All of the source code, along with all the associated header files, is run through the compiler to generate object files. Then all the object files are linked together to create a library file.

click to expand

The main reason for all these header files is that when the class is implemented, all the classes, structures, variables, and so on are defined and thus are accessible.

This exact process can be used to generate library assemblies as well. The only difference in the process would be that the Managed C++ flags are turned on for the compiler and linker. Because this method of creating libraries is so commonplace, I will show you how it's done here so that when you see it—and you will see it—you will understand what is happening. Later in the chapter, I will show you a better solution for building library assemblies.

The following example, which consists of Listings 4-3 through 4-6, shows how to create an assembly using the traditional C++ method.

Listing 4-3: Card.h: Traditional Method

start example
 namespace Cards {     public __value enum Suits { Heart, Diamond, Spade, Club };     public __gc class Card     {         Int32 _type;         Suits _suit;     public:         Card(Int32 type, Suits suit);         __property Int32 get_Type();         __property Suits get_Suit();         virtual String *ToString();     }; } 
end example

Listing 4-3 shows the header definition to the Card.h file. This file defines an enum of playing card Suits and a Card class within the namespace of Cards. Notice that the keyword public is placed in front of both the enum and the class, as both need to be publicly accessible.

Listing 4-4 shows the implementation of the class's constructor and member methods. The only thing of note in this file is that you override the virtual method ToString(); as you can see, there is nothing special to doing this.

Listing 4-4: Card.cpp: Traditional Method

start example
 #using <mscorlib.dll> using namespace System; #include "card.h" Cards::Card::Card(Int32 type, Suits suit) {     _type = type;     _suit = suit; } Int32 Cards::Card::get_Type() {     return _type; } Cards::Suits Cards::Card::get_Suit() {     return _suit; } String *Cards::Card::ToString() {     String *t;     if (_type > 1 && _type < 11)         t = _type.ToString();     else if (_type == 1)         t = S"A";     else if (_type == 11)         t = S"J";     else if (_type == 12)         t = S"Q";     else         t = S"K";     switch (_suit)     {         case Heart:             return String::Concat(t, S"H");         case Diamond:             return String::Concat(t, S"D");         case Spade:             return String::Concat(t, S"S");         default:             return String::Concat(t, S"C");     } } 
end example

Listing 4-5 defines a second class named Deck. Notice that you use the Card class within the class, yet you never declare it within the header file. The trick to handling this is to remember that header files are basically pasted wholesale into the source file during compilation. Because this is the case, you simply place the include file of Card.h before Deck.h in the Deck.cpp source file, as you will see in Listing 4-6. Thus, the Card class is pasted in first and, therefore, defined as needed before the Deck class.

Listing 4-5: Deck.h: Traditional Method

start example
 namespace Cards {     public __gc class Deck     {         Card *deck[];         Int32 curCard;     public:         Deck(void);         Card *Deal();         void Shuffle();     }; } 
end example

Listing 4-6: Deck.cpp: Traditional Method

start example
 #using <mscorlib.dll> using namespace System; #include "card.h" #include "deck.h" Cards::Deck::Deck(void) {     deck = new Card*[52];     for (Int32 i = 0; i < 13; i++)     {         deck[i]    = new Card(i+1, Suits::Heart);         deck[i+13] = new Card(i+1, Suits::Club);         deck[i+26] = new Card(i+1, Suits::Diamond);         deck[i+39] = new Card(i+1, Suits::Spade);     }     curCard = 0; } Cards::Card *Cards::Deck::Deal() {     if  (curCard < deck->Count)          return deck[curCard++];      else          return 0; } void Cards::Deck::Shuffle() {     Random *r = new Random();     Card *tmp;     Int32 j;     for( int i = 0; i < deck->Count; i++ )     {         j         = r->Next(deck->Count);         tmp       = deck[j];         deck[j] = deck[i];         deck[i] = tmp; } curCard = 0; } 
end example

Listing 4-6 shows the final source file to the minilibrary. Notice, as I stated previously, that Card.h is included before Deck.h. If you're observant, you might also notice that the Random class is used. You can find this class within the.NET Framework class library.

The command you need to execute to build a library assembly from the command line is a little more complex than what you have seen so far, but it is hardly rocket science. The syntax is simply as follows (without the ellipsis):

 cl source1.cpp source2.cpp...sourceN.cpp /CLR /LD /o OutputName.dll 

The first change to the command line is that it takes a list of source file names. The next change is the /LD argument, which tells the linker to create a .dll and then, finally, the /o argument, which indicates the name of the .dll file to create.

To compile the previous example, you would use

 cl card.cpp deck.cpp /CLR /LD /o cards.dll 

Building Multifile Library Assemblies using the New Assembly Method

Personally, maintaining header files is a big pain. And I am happy to see them disappear because of .NET's new assemblies. I always seem to forget one or more and have to search for their names. They have to be stored off on a disk someplace. Splitting source code in half increases the risk of something going missing or getting out of sync during the development process.

Because the splitting of header and source files is not needed, it is possible to take a new approach to coding a library. After playing around a bit, I came up with this simplified method. First code all classes, both definition and implementation, using only class (.h) files. Then, using a single linker (.cpp) file, include all the .h files. The only tricky part to this method is making sure that you place the .h files in the right order so that everything is defined before it is used, but this same problem must also be dealt within the traditional method.

click to expand

With this method, you need to maintain half the number of files and, because definition and source are defined together, they cannot get out of sync. The combination makes maintaining the library far easier. Add to this the fact that it is easier to read and documentation is only required in one place and, I think, this method is better. The only drawback I see is that it is different for someone who has been coding C++ for a while, so it may take a while to catch on.

Listings 4-7 through 4-9 show the same library you built previously, except that it uses my new approach. As you can see, Listing 4-7 is a combination of the traditional method's Card.h and Card.cpp files. Basically, instead of defining the methods in a separate file, I just place the definitions directly within the class definition.

Listing 4-7: Cards.h: Assembly Method

start example
 namespace Cards {     public __value enum Suits { Heart, Diamond, Spade, Club };     public __gc class Card     {         Int32 _type;         Suits _suit;     public:         Card(Int32 type, Suits suit)         {             _type = type;             _suit = suit;         }         __property Int32 get_Type()         {             return _type;         }         __property Suits get_Suit()         {             return _suit;         }         virtual String *ToString()         {             String *t;             if (_type > 1 && _type < 11)                 t = _type.ToString();             else if (_type == 1)                 t = S"A";             else if (_type == 11)                 t = S"J";             else if (_type == 12)                 t = S"Q";             else                 t = S"K";             switch (_suit)             {                 case Heart:                     return String::Concat(t, S"H");                 case Diamond:                     return String::Concat(t, S"D");                 case Spade:                     return String::Concat(t, S"S");                 default:                     return String::Concat(t, S"C");             }         }     }; } 
end example

Similarly, Listing 4-8 is a combination of the traditional Deck.h and Deck.cpp. Notice that as in the traditional method, you are accessing other classes within this class without having defined them within the file. Obviously, the linker file needs to make sure that it places all class definitions in the right order so that they will be defined before they are used.

Listing 4-8: Deck.h: Assembly Method

start example
 namespace Cards {     public _gc class Deck     {         Card  *deck[];         Int32 curCard;     public:         Deck(void)         {             deck = new Card*[52];             for (Int32 i = 0; i < 13; i++)             {                 deck[i]    = new Card(i+1, Suits::Heart);                 deck[i+13] = new Card(i+1, Suits::Club);                 deck[i+26] = new Card(i+1, Suits::Diamond);                 deck[i+39] = new Card(i+1, Suits::Spade);             }             curCard = 0;         }         Card *Deal()         {             if (curCard < deck->Count)                 return deck[curCard++];             else                 return 0;         }         void Shuffle()         {             Random *r = new Random();             Card *tmp;             Int32 j;             for( int i = 0; i < deck->Count; i++ )             {                 j         = r->Next(deck->Count);                 tmp       = deck[j];                 deck[j] = deck[i];                 deck[i] = tmp;             }             curCard = 0;         }     }; } 
end example

The linker file is the only unusual file, as it contains nothing but include statements. It is needed because the Managed C++ compiler only compiles .cpp files; therefore, to have the compiler do anything, you need to have this file (see Listing 4-9). A convenient side effect of this file is that it is easy to add or remove classes from the library, and you have documented in a single place all the classes that make up the library.

Listing 4-9: Cards.cpp: Assembly Method

start example
 #using <mscorlib.dll> using namespace System; #include "card.h" #include "deck.h" 
end example

Another thing that you should notice about the linker file is that it is a great location to place testing code during development.

An added bonus to this method of building an assembly library is that the command to run from the command line is simpler:

 cl cards.cpp /CLR /LD 




Managed C++ and. NET Development
Managed C++ and .NET Development: Visual Studio .NET 2003 Edition
ISBN: 1590590333
EAN: 2147483647
Year: 2005
Pages: 169

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