수안이의 컴퓨터 연구실

  • Mainpage
  • About Me
  • Tags
  • Metapage
  • Notice
  • Location
  • Keywords
  • Guestbook
  • Admin
  • Write an Article
  • Total | 1620471
  • Today | 333
  • Yesterday | 670

128 Articles, Search for 'Programming/C++'

  1. 2008/04/26 C++ Preprocessor: Always Assert Your Code Is Right
  2. 2008/04/26 C++ Preprocessor: The Code in the Middle
  3. 2008/04/26 C++ Programming Tips
  4. 2007/07/30 C++ Preprocessor: Always Assert Your Code Is Right
  5. 2007/07/30 C++ Preprocessor: The Code in the Middle
  6. 2007/07/30 Multithreading in C++
  7. 2007/07/30 A Simple Garbage Collector for C++
  8. 2007/07/30 Function Pointers (1)
  9. 2007/07/27 DLL Conventions: Issues and Solutions
  10. 2007/07/27 More on Handling Basic Data Types
«Prev  1 2 3 4 5 ... 13  Next»
Programming/C++2008/04/26 18:20

C++ Preprocessor: Always Assert Your Code Is Right

Site : http://www.devarticles.com/c/a/cplusplu ··· right%2F

Are you looking for a way to speed up the debugging process in C++? This article explains how to use asserts to do just that, showing that not all macros are evil.

If a man begins with certainties, he shall end in doubts;

But if he will be content to begin with doubts,

 He shall end in certainties.

[Francis Bacon 1561-1626]

Assertive Programming

If there is one thing I have learned over the past few years, it is not to underestimate the power of assert(). It comes with your compiler and can be found either in cassert or assert.h.

The reason I love assert is because it looks after me and helps me find bugs I was sure weren’t there. I mean, we all write bugfree software, right? Yeah right. Well there have been many Kodak moments when an assert fired on something I was dead sure could never happen; I should be a rich man as the look on my face was priceless.

If you are not familiar with asserts, you should start using them right now. Use  assert to check that your code is working the way you expect it to or be prepared to pay the price. No not for my face, but for long nights behind the debugger, ticking away possible problem sources until you have your own Kodak moment.

When do you use assert, you ask? Simple… whenever you can use it to verify the truth of a situation: ‘this pointer can never be null’, ‘this number is never smaller than zero’, ‘there is always at least one customer stored in this list’, ‘this code won’t be used the next millennium, two digits are fine’.

Use proper error handling when you can check for things that can go wrong. Use asserts on things you are sure can’t go wrong.

Trust me… the sillier the assert… the more valuable it is. I tested this once myself, grinning, thinking ‘this is ridiculous, want to bet it will never fire?’, only to have it triggered a couple of months later! And because the assert was so preposterous and silly I immediately knew which conditions were breaking my code.

assert.

  1. To state or express positively, affirm: asserted his innocence
  2. To defend or maintain (one’s rights, for example)

The important thing to remember is that asserts are only compiled into your code when the NDEBUG macro is not defined. So in the final optimized version of your application, they are not there to bloat or to slow things down! The only investment you have to make is typing them out while you are working on those classes and functions. It will pay you back greatly when it helps you shorten the time it takes to track down bugs.

Face it… we all write code on assumptions we make about the situation the code will be running in. Assert you are in that situation, because your code base will grow beyond the picture you have of it in your mind.

Implementing Assert

Though we will be implementing our customized version of assert here, all you need to do is include assert.h and you are ready to use the assert macro that comes with it:

The __assert helper function prints an error message to stderr and the program is halted by calling abort(). It is possible that the implementation that comes with your compiler varies slightly, but you get the idea.

Lets use a simple example function:

In the unfortunate situation that the pointer to string is NULL, execution will halt and you will be offered the possibility of opening the debugger and jumping to the location in the source where the assertion failed. This can be very handy as you can examine the call stack, memory, registers, and so forth, and are likely to catch the perpetrator red handed!

Implementing a Simplified Assert

Although I am ranting about why you should use assert, this article is aimed at showing you how to implement your own version using preprocessor macros.

Here is a very basic version that doesn’t even halt execution:

Notice that we don’t need a helper function to get information onto the screen. (I am sticking to printf in the following examples, but there is nothing stopping you from using fprintf and stderr instead.) The stringize macro operator (#) names the condition for us in the printf statement, adding quotes as well and the predefined __LINE__ and __FILE__ macros help to identify the location of the assert.

The do { } while ( false ) statement for the unused version of assert, I used to make sure that the user cannot forget to conclude his assert statement with a semicolon. The compiler will optimize it away, and I consider it a slightly better way to say that I am not doing anything.

We are only one step away from halting our application’s execution (don’t forget to include stdlib.h):

In case writeString is called with a NULL pointer now, we are told what went wrong, where it went wrong and the application is halted.

When you are using the Microsoft Visual C++ 7.1 compiler, you’ll be presented with a dialog screen:

After pressing Retry you are presented with more dialogs and finally... you end up in the debugger.

The problem with abort() is that it doesn’t let you resume execution after you’ve concluded that the assert was benign; or maybe you are interested in what happens immediately after the assert? You need to disable the assert (never a good thing), recompile and restart your application.

There are other ways to halt execution and you will have to look in your compiler’s documentation to discover what the correct call is, but with Visual C++ it’s an interrupt call:

When our application halts again on the writeString function, we’ll encounter the following dialog (did you recognize it? We actually came across this dialog when halting the application with the abort() function!) :

It is not a problem to continue execution now, if we think this is responsible… that is you are often recommended to implement your own assert functionality.

Wouldn’t it be nice to be able to add some hints or remarks along with the location of the assert? When the assert fires and you don’t have a debugger available, this message might still tell you what the problem is:



Refining Assert

The simplest solution is to just expand the assert macro and make it accept a message as well as a condition:



Again the stringize operator helps us to stuff the message we want into the printf statement. We could call it a day now, but I am a very lazy coder… I do not want to be forced to put messages into the assert, sometimes I just want to assert.

Of course it is possible to write an ASSERT(condition) and an ASSERTm(condition, message) macro, but did I mention I am a forgetful coder too? I’d much rather have a single ASSERT statement that can do both.

The first thing that comes to mind is the fact I could do this easily with a function:

So maybe if I declared another function:

void NoAssert(bool isOK, char const *message=””) {}

And then defined assert as:

While this seems like a quick solution, I have completely lost my extra debug information! The line information is the same now for every assert… oh… the compiler substitutes __LINE__ with the actual line number it is compiling at that moment, and since we are making a function call – all line numbers lead to the MyAssert function!

Alexandrescu demonstrates a great way around this problem [Alexandrescu]. (It is also a great article showing how you can take assertions to a higher level after this one, by making them throw exceptions!)



For some reason my Visual C++ 7.1 compiler will not accept the __LINE__ macro next to the __FILE__ macro in the code above. The strange thing is that the __FILE__ macro works fine, but with __LINE__ it complains:

It is never easy, but I do not want to give up at this point. Since the macro is expanded as a single line into the place where we are calling it, and since the compiler apparently has no objection to me assigning __LINE__ as a default parameter in a constructor, let's try again:

Now that we have our line information back, we are nearly there; as soon as we add a second assert, the compiler complains that we are redefining the struct MyAssert! If only we could keep the struct declaration local… and again Alexandrescu shows us how [Alexandrescu]:



There is a lot of fun to be had with macros and sometimes it is possible to create the wildest incantations with them [Niebler/Alexandrescu]. I hope you are convinced that despite the fact that they can be considered evil, there is something magical about them as well.

As a final example I will show you how to create personal and customizable debug streams in the next article… all with macros.

References

 [STL] – The Standard Template Library
< comes standard with your compiler but this one is very portable>
http://www.stlport.org

[BOOST] – Boost C++ Libraries
http://www.boost.org

[ACE] – The ADAPTIVE Communication Environment
http://www.cs.wustl/edu/~schmidt/ACE.html

[Niebler] – Eric Niebler
“Conditional Love: FOREACH Redux”
http://www.artima.com/cppsource/foreach.html

[Alexandrescu] – Andrei Alexandrescu
Assertions
http://www.cuj.com/documents/s=8248/cuj ··· xandr%2F

"C++" 카테고리의 다른 글
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2008/04/26
  • C++ Preprocessor: The Code in the Middle (0)2008/04/26
  • C++ Programming Tips (0)2008/04/26
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2007/07/30
  • C++ Preprocessor: The Code in the Middle (0)2007/07/30
2008/04/26 18:20 2008/04/26 18:20
Posted by webdizen
Tags C++Keyword C++, Preprocessor
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3208

Leave your greetings.

[로그인][오픈아이디란?]

Programming/C++2008/04/26 17:57

C++ Preprocessor: The Code in the Middle

Site : http://www.devarticles.com/c/a/cplusplu ··· iddle%2F

In this article, we examine instructions given to the preprocessor and see how they are used in general. The preprocessor handles your code before the compiler interprets it. If you have been wondering just what the preprocessor is used for, this article explains.

Whence and what art thou, execrable shape?

[John Milton 1608-1674]

The Preprocessor and the Compiler

Before the compiler interprets your code, the preprocessor handles it. Its task is to scan through your code and to look for preprocessor instructions; these can for example be used to replace certain tokens with string or numerical values, or to blank out complete sections of the code before the compiler sees it. You can recognize these instructions from the pound (#) symbol that needs to precede every preprocessor instruction.

We are going to examine these instructions and see how they are used in general. Most probably you have been using at least one of them without giving it much thought: ‘#include’. Still, this is an instruction that is handled by the preprocessor and not by the compiler. It tells it to replace the directive with the contents of the file named after it.

One of the problems a large C++ project can face is long compilation time. This can happen when ‘#include’ is used carelessly; to compile a single source, the preprocessor might (indirectly) be pasting thousands of lines of headers in front of it!

When you use third party libraries like STL-port or Boost, you will find that macros can be very useful for building different code configurations. If you have to write portable C++ code you won’t be able to live without them… however you will most probably have grown to dislike them as well.

Don’t be misled by the fact that preprocessor macros may look deceptively simple. As I’ve said before, with C++ things can become as complex as you want them to be, and to be frank, a lot of great code I have seen involved the use of macros one way or another. Maybe this is because they provide the possibility of extending the language in such a cheeky (and sneaky) way.

Let's not get ahead of ourselves. We'll first look at what the preprocessor is used for most: string substitution and conditional compilation.

String Substitution

The preprocessor literally changes your code (following the instructions you provide of course) and the result is a new source file, a temporary file that you cannot see but which is fed to the compiler. To specify which string tokens you want to have substituted you use the ‘#define’ directive.

For example, when you want to make use of arrays, you will have to specify how big you want one to be. By defining a MAX_STR_LEN token, you can define how big this array is once, instead of having to repeat this value throughout your code.

It is usually not a good idea to use ‘#define’ as a substitute for constants; the preprocessor merely does a string substitution and doesn’t perform any typechecking. Depending on your debugger it can make it difficult to interpret values during a debug session.

In this case it would have been better to use a constant value:

There are programmers that use ‘#define’ to specify constants, so be wary when you detect these. Still I think it is better to use a ‘#define’ then it is to litter your values magically throughout the code.

Imagine that the maximum string length ise shrunk to 72,.and somewhere in the code there is a for-loop that was overlooked, and would still loop until 80. An overflow has been created and worse yet, this may only become clear when the application is run in optimized mode! The loop will modify a section of the memory, causing your application to crash in a mysterious way… oh the agony when you discover the overlooked for-loop.

Sometimes string substitution can be used to help makes things clearer as well… though some consider the following deviation to be distracting:



String Manipulation

When you use the preprocessor for string substitution it can do more than simply replace tokens with values. It is possible to quote strings that were passed as a parameter and you can concatenate two strings together into one.

The stringizing operator (#) substitutes the macro variable following it with its value and puts quotes around it.

For example:

When used like:

will be substituted into and presented to the compiler as:

With the concatenation operator (##) you can tie separate strings into a new single identifier. There are many useful things you can do with this and we’ll actually need this operator when we implement customizable streams.

It is also possible to drive things to extremes. There is a company I know of that requires in its coding standard that you use their ‘Simplified Hungarian naming convention’. This means that a function that takes a const reference to an object MyClass doesn’t look like:

But in their code looks like:

While this might save you some typing effort, I wouldn’t recommend making such drastic changes to the C++ language. Still as an example… how did it ever get this far?

Now you’ve made the following code possible:

Macro Functions

The ‘#define’ directive is very versatile and powerful, given the simple fact that it can replace one string with another string: we can create macro functions.

The MIN and MAX macros are also often quoted as examples:

You have to remember that all this does is string substitution, and any form of type checking is completely lacking!

An important note is that the opening parenthesis for the parameter list must immediately follow the macro name. There are no spaces allowed, since the preprocessor will otherwise consider it to be a regular string substitution.

In case you wonder why the parameters are all wrapped in parenthesis again, consider what would happen if we’d defined SQUARE as:

Now why does the following give a result of 11 instead of 25?

Ouch this is not what we intended:

But using parenthesis it is correct again:

Conditional Compilation

One more way to use ‘#define’ is to declare that a particular token is defined. This token can be used in combination with the precompiler commands “#if”, “#elif”, “#else” and “#endif” to test whether or not the preprocessor should include the code that follows the directive.

To see if a token is defined you use “#if defined” or “#elif defined” and to see if a token is not defined you use “#if !defined” or “#elif !defined”.  You can use the logical ‘&&’ and ‘||’ operators to concatenate instructions.

If for some reason you would like to remove a token from the table the preprocessor stores it in, you can undefine it with ‘#undef’.

A common use is to define a token DEBUG when compiling non-optimized code. If you use the Microsoft Developer Environment, a token _DEBUG is automatically defined for your project’s Debug configuration.



First the precompiler runs into the ‘#include’ instruction, so it reads the ‘stdio.h’ file (which it can find using your project’s header include path) and pastes it into the temporary source file it is going to feed to the compiler. Next it encounters the ‘#define’ instruction and it stores the MYDEBUG token in a lookup table.

Finally when it comes across the ‘#ifdef’ instruction it checks whether MYDEBUG was defined in its lookup table and when this is the case it will continue to write everything it finds until the ‘#endif’ instruction into an intermediate source file.

When it cannot find the token it will check whether the next instruction is another comparison “#elif”, tells it to follow that route otherwise “#else” or that the end of the block was reached “#endif”.

If you look at the macros used to configure STL-Port (STL), Boost (BOOST) or ACE (ACE), things can become rather complex. Handy as macros can be, they do not make the code easer to read (and thus understand). Here is a snippet from the boost thread class:

Because the macros are obfuscating which variables are needed here (because different platforms offer different facilities), it is easy to overlook that on Linux this would filter into:

When you want your code to be portable across different platforms, preprocessor macros will help you to maintain a single source tree. Unfortunately your eyes will have to deal with all possibilities, instead of the single one that your compiler might be choking on.

When there are so many trees it becomes difficult to see the forest.

Inclusion Guards

As your application grows, you will have to organize your code among multiple source and header files. All these sources are compiled into object files, which are linked together into possibly a single executable.

Now that you are using multiple classes, many header files will have to be included into each source file. Sometimes header files will include other header files, and it might be possible that one header file will be included multiple times into a single source file.

Let’s create the ubiquitous Animal class from which we’ll derive a Hippopotamus and a Crocodile. Both classes will have to include the Animal header to be able to derive from Animal. To make things easier for whoever wants to play with a Hippopotamus and/or a Crocodile, we’ll include the Animal class in both respective header files.

Now if you are going to use them together, you’ll be including the Animal header twice. Unless you make sure it is included only once, the compiler will start complaining about multiple definitions. This is why we need inclusion guards:

When the preprocessor tries to include the animal header more than once, it will find that _ANIMAL_H is already defined in its definitions table and skip the header. There is a preprocessor directive that takes care of this for the Microsoft compiler: ‘#pragma once’, but you have to remember that this makes your headers non-portable. So why not stick to inclusion guards instead?

Displaying the Intermediate Form

One of the problems with macros is that it can be very hard to deduce what the state of the preprocessor will be when it reaches a certain directive. If you’ve tried porting STL-port across different platforms, you will know what I am talking about: somewhere at the start a definition is put into the preprocessor’s table, cascading through different header files, down to the point where you are interested to see whether a piece is included in the final source that goes to the compiler or not.

Oh the hairs I would have pulled out of my head if it had not been possible to tell the compiler to show us what it sees. Start a shell and make sure you can use your compiler from the command line. If you are using the Microsoft compiler the following command will dump the output from the preprocessor :

When you are using gcc it can be as simple as:

Just invoke the help on the compiler to see which command line argument you’ll need. Remember, as your project grows more complex, you will have to provide the inclusion paths to headers that are not present in the same directory.

Predefined Macros

Most compilers come standard with a number of macros predefined: __DATE__ , __TIME__ , __LINE__ and __FILE__ .  These macros are very useful when you have to debug different builds. The date and time macros are translated to the date and moment the source was compiled. The line macro translates to the line number it appeared on in the source file and the file macro is translated to the actual filename of the source.

You will see that these macros come in handy when we start customizing some assert functionality.

Macro Troubles

Hopefully you are aware that macros are evil and can cause you a lot of headaches. Even though they have their clear benefits, they can cause a lot of trouble as well. There are four types of problems you can expect when using them.

Macros are not type-safe. When you find that typesafety might be an issue when you are implementing a macro function, you might want to consider replacing it with a templated function.

Macros have to be defined on one line. Things can become pretty complex and hard to read if your macro function extends over several lines. (You can make your macro span several lines by adding a backslash character (\) at the end of every line, except for the last).

Macros perform simple string substitution. This means that there is no such thing as a macro function call; the code defined in the macro is going to be pasted wherever that macro is used. This can cause considerable bloat in your source.

Because macros are expanded inline, not all debuggers can display the contents of the macro during a debug session. This means that you have no means to verify the way your macro function is being executed. This can lead to a lot of pain, only to discover you’ve forgotten a couple of parentheses!

As rough and coarse they may be, there are many nice tricks you can perform with preprocessor macros. Just take a look at the excellent crafted BOOST_FOREACH [Niebler] macro.

You will find a whole new dimension when you are able to combine macros and templates in meaningful ways. Soon I will be able to present to you the Active Object pattern and a nice macro that simplifies the implementation of this pattern using the ACE [ACE] library.

But first things first. Next time we’ll create a custom assert.

References

[STL] – The Standard Template Library
< comes standard with your compiler but this one is very portable>
http://www.stlport.org

[BOOST] – Boost C++ Libraries
http://www.boost.org

[ACE] – The ADAPTIVE Communication Environment
http://www.cs.wustl/edu/~schmidt/ACE.html

[Niebler] – Eric Niebler
“Conditional Love: FOREACH Redux”
http://www.artima.com/cppsource/foreach.html

[Alexandrescu] – Andrei Alexandrescu
Assertions
http://www.cuj.com/documents/s=8248/cuj ··· xandr%2F

"C++" 카테고리의 다른 글
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2008/04/26
  • C++ Preprocessor: The Code in the Middle (0)2008/04/26
  • C++ Programming Tips (0)2008/04/26
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2007/07/30
  • C++ Preprocessor: The Code in the Middle (0)2007/07/30
2008/04/26 17:57 2008/04/26 17:57
Posted by webdizen
Tags C++Keyword C++, Compiler, Preprocessor
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3207

Leave your greetings.

[로그인][오픈아이디란?]

Programming/C++2008/04/26 15:56

C++ Programming Tips

Site : http://www.devarticles.com/c/a/cplusplu ··· ing-tips

In this article, I give you programming tips that can improve your C++ code. I will cover use of parentheses, long expressions, modifying objects, and more. By the time I'm done, you will hopefully have learned a few ways to make your code faster as well as easier to understand and maintain.

C++ Fundamentals

Use of parentheses

Consider this expression without parentheses:

and the equivalent with parentheses:

Both expressions perform exactly the same computation, but the meaning of the second one is clearer, since parentheses are used.

Expressions (like the two above) are evaluated by following the rules of precedence. The above expressions are the same. So from a technical point of view, you do not need parentheses. However, including parentheses makes the order of evaluation explicit to the person maintaining your code (even you).

Long expressions (Style)

We often need to write long expressions that will not fit on one line of the screen. For example, the expression

is quite long and will not fit on one line of the screen. A good way to break a long expression into a multiline expression is to use continuation lines. These lines should always begin with an operator and be indented one space. Both of these serve to signal the reader that the line is a continuation of the previous line. So the above example can be written as follows:



As another example, the expression

can be better written as

Speed versus clarity and correctness

One rule of thumb in programming is that 90 percent of a program’s running time is spent on 10 percent of the code. So without some idea of where a program spends most of its time, most changes to a program to speed it up will have little or no effect on the overall running time. While speed is important, clarity (presentation of the code) and correctness are always more important. We should never sacrifice clarity and correctness for speed.

Hot spots are where a program spends most of its time. If speed becomes an issue, a more effective approach is to complete the writing of the program and then order a tool which can be used to identify the hot spots of the program. The hot spots can then be tuned (recoded) to reduce the running time of the program.

Increment and decrement operators (style)

Always use increment and decrement operators to reduce the length of your code. This also portrays you as a mature programmer.

So for example, you should write

instead of

or write

instead of

where i is a C++ variable.

Watch out for operator precedence on long expressions.

Function Usage Basics and Libraries

Controlling the evaluation of assert invocations

The default behavior of an assert invocation is to evaluate assertions. The macro NDEBUG must be undefined for the expression in an assert invocation to be evaluated. Through the use of the following preprocessor statement, the evaluation of subsequent assertions can be turned off.

In some cases, evaluation of assertions can be turned on again through the use of the following preprocessor statement:

This process works because the actions that an assert invocation performs are nested within a conditional evaluation directive that checks whether NDEBUG is defined. It is a good practice to include the definition of NDEBUG at the start of a program that is distributed to users.

Safe input type

Professional software normally extracts its input in character form. This input is then validated and converted to the desired form (e.g. int). This provides a safe way of extracting data. If you do not respect this, you may suffer from the following consequence: if an int is required and the user accidentally types in a non-digit, the program might terminate with an error message that the user does not understand.

Efficiency in Using Constant Reference Parameter

If you pass an object by value to a function, a copy of the object is first made and then passed to the called function. For large objects, copying can add considerable overhead to the program and may also adversely affect the performance of the program. For such objects, you can obtain efficiency and retain safety by passing the object as a constant reference parameter, such as the following:

ReturnValue functionName(const type &objectName)

The const modifier ensures that the called function does not modify the actual object.

Abstract Data Types

Developing object-oriented programs is typically simpler than developing procedure oriented programs. For example, member functions generally have a smaller parameter list than non-member functions because there is no need to pass the data to member functions.

Since well-written member functions ensure that the data members are correctly initialized and updated, other program functions do not need to validate whether they are using proper object values. The access protection provided by encapsulation also ensures that clients use the public interface and not side effects of the library implementation that may continue as the implementation is modified over time.

Rule of minimality

The rule of minimality states that unless a behavior is needed, it should not be part of the Abstract Data Type. A corollary of this rule is the Class Minimality Principle, which states that if a function or operator can be defined such that it is not a member of the class, then do not make it a member. This practice makes a non-member function or operator generally independent of changes to the class’s implementation.

Character strings or string class strings

If you use the string class, you have a more extensive set of capabilities than if you use character strings.

Generic vector extraction

Consider the following:


The syntax template <class T> indicates that what follows is a generic form. In this situation, the form of the function is given. This function template allows different functions to be instantiated based on the particular type of vector used as the actual parameter. For example, if we have the following definitions:

to be defined and invoked.


Accessing multidimensional arrays

Multidimensional arrays are stored in row-major order. Often contiguous sections of memory, called pages, are brought in with the expectation that values near a desired value are more likely to be referenced than values defined elsewhere in memory. Since a page is contiguous memory, it is more likely to contain a complete row than a complete column. So it is generally more efficient to process array elements row-by-row rather than column-by-column. This efficiency comes about in how memory values are brought into the central processing unit, as explained above.

Pointer strength

The ability to access and change the value of an object whose name is not part of the assignment statement is the feature that makes pointer objects so powerful. This capability is most often used for dynamic objects.

Inheritance: Using data member initialization list

There are two ways to initialize data members when implementing the constructor of a class. One way is to invoke the constructor for the data member via the data member initialization list. The other method is to invoke the data member’s mutator from within the body of the constructor. With the second method, the data member’s default constructor is invoked to construct an object with default values, then the mutator is called to overwrite the default values with initial values. Using the first method, the data member is constructed with the appropriate initial values. As you can see the second method is more efficient, because it is done during construction.

Templates and Polymorphism

Automatic subscript checking for vector elements and other list elements

The vector class template provides a member function, which is:

at().

The function expects an index value as its parameter, say i. If the index is valid (exists), a reference to the ith element of the vector is returned. Otherwise, an exception is generated; the program stops, giving an error message that the user does not understand. You can use inheritance to derive a class that overloads the subscript operators so that index checking is automatically performed in conjunction with the subscript operators. To solve this problem, a template class named protectVector is given below.



Assume reuse

If you are defining a class, unless you know it can never serve as a base class for a derive class, give the class a virtual destructor. In this way, all derived destructors will also be virtual and then, whenever an object is deleted, the correct destructor will be invoked, regardless of the context.

All these tips are what I have gathered, based on my experience and the experience of expert programmers. Here's hoping you put them to good use.

"C++" 카테고리의 다른 글
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2008/04/26
  • C++ Preprocessor: The Code in the Middle (0)2008/04/26
  • C++ Programming Tips (0)2008/04/26
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2007/07/30
  • C++ Preprocessor: The Code in the Middle (0)2007/07/30
2008/04/26 15:56 2008/04/26 15:56
Posted by webdizen
Tags C++Keyword C++, Programming
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3206

Leave your greetings.

[로그인][오픈아이디란?]

Programming/C++2007/07/30 10:34

C++ Preprocessor: Always Assert Your Code Is Right

출처 : http://www.devarticles.com/c/a/cplusplu ··· right%2F

Are you looking for a way to speed up the debugging process in C++? This article explains how to use asserts to do just that, showing that not all macros are evil.

If a man begins with certainties, he shall end in doubts;

But if he will be content to begin with doubts,

He shall end in certainties.

[Francis Bacon 1561-1626]

Assertive Programming

If there is one thing I have learned over the past few years, it is not to underestimate the power of assert(). It comes with your compiler and can be found either in cassert or assert.h.

The reason I love assert is because it looks after me and helps me find bugs I was sure weren’t there. I mean, we all write bugfree software, right? Yeah right. Well there have been many Kodak moments when an assert fired on something I was dead sure could never happen; I should be a rich man as the look on my face was priceless.

If you are not familiar with asserts, you should start using them right now. Use  assert to check that your code is working the way you expect it to or be prepared to pay the price. No not for my face, but for long nights behind the debugger, ticking away possible problem sources until you have your own Kodak moment.

When do you use assert, you ask? Simple… whenever you can use it to verify the truth of a situation: ‘this pointer can never be null’, ‘this number is never smaller than zero’, ‘there is always at least one customer stored in this list’, ‘this code won’t be used the next millennium, two digits are fine’.

Use proper error handling when you can check for things that can go wrong. Use asserts on things you are sure can’t go wrong.

Trust me… the sillier the assert… the more valuable it is. I tested this once myself, grinning, thinking ‘this is ridiculous, want to bet it will never fire?’, only to have it triggered a couple of months later! And because the assert was so preposterous and silly I immediately knew which conditions were breaking my code.

assert.

  1. To state or express positively, affirm: asserted his innocence
  2. To defend or maintain (one’s rights, for example)

The important thing to remember is that asserts are only compiled into your code when the NDEBUG macro is not defined. So in the final optimized version of your application, they are not there to bloat or to slow things down! The only investment you have to make is typing them out while you are working on those classes and functions. It will pay you back greatly when it helps you shorten the time it takes to track down bugs.

Face it… we all write code on assumptions we make about the situation the code will be running in. Assert you are in that situation, because your code base will grow beyond the picture you have of it in your mind.

Though we will be implementing our customized version of assert here, all you need to do is include assert.h and you are ready to use the assert macro that comes with it:

// assert.h
#ifndef NDEBUG
void __assert(const char *, const char *, int);
#define assert(e) \
     ((e) ? (void)0 : __assert(#e,__FILE__,__LINE__))
#else
#define assert(unused) ((void)0)
#endif

The __assert helper function prints an error message to stderr and the program is halted by calling abort(). It is possible that the implementation that comes with your compiler varies slightly, but you get the idea.

Lets use a simple example function:

void writeString(char const *string) {
assert( 0 != string );
...
}

In the unfortunate situation that the pointer to string is NULL, execution will halt and you will be offered the possibility of opening the debugger and jumping to the location in the source where the assertion failed. This can be very handy as you can examine the call stack, memory, registers, and so forth, and are likely to catch the perpetrator red handed!

Although I am ranting about why you should use assert, this article is aimed at showing you how to implement your own version using preprocessor macros.

Here is a very basic version that doesn’t even halt execution:

#ifndef NDEBUG
# define ASSERT( isOK ) \
( (isOK) ? \
     (void)0 : \
     (void)printf(“ERROR!! Assert ‘%s’ failed on line %d ” \
      “in file ‘%s’\n”, #isOK, __LINE__, __FILE__) )
#else
# define ASSERT( unused ) do {} while( false )
#endif

Notice that we don’t need a helper function to get information onto the screen. (I am sticking to printf in the following examples, but there is nothing stopping you from using fprintf and stderr instead.) The stringize macro operator (#) names the condition for us in the printf statement, adding quotes as well and the predefined __LINE__ and __FILE__ macros help to identify the location of the assert.

The do { } while ( false ) statement for the unused version of assert, I used to make sure that the user cannot forget to conclude his assert statement with a semicolon. The compiler will optimize it away, and I consider it a slightly better way to say that I am not doing anything.

We are only one step away from halting our application’s execution (don’t forget to include stdlib.h):

#ifndef NDEBUG
# define ASSERT( isOK ) \
if ( !isOK ) { \
 (void)printf(“ERROR!! Assert ‘%s’ failed on line %d “ \
  “in file ‘%s’\n”, #isOK, __LINE__, __FILE__) ); \
 abort(); \
}
#else
# define ASSERT( unused ) do {} while ( false )
#endif

In case writeString is called with a NULL pointer now, we are told what went wrong, where it went wrong and the application is halted.

When you are using the Microsoft Visual C++ 7.1 compiler, you’ll be presented with a dialog screen:


After pressing Retry you are presented with more dialogs and finally... you end up in the debugger.

The problem with abort() is that it doesn’t let you resume execution after you’ve concluded that the assert was benign; or maybe you are interested in what happens immediately after the assert? You need to disable the assert (never a good thing), recompile and restart your application.

There are other ways to halt execution and you will have to look in your compiler’s documentation to discover what the correct call is, but with Visual C++ it’s an interrupt call:

__asm { int 3 }

When our application halts again on the writeString function, we’ll encounter the following dialog (did you recognize it? We actually came across this dialog when halting the application with the abort() function!) :

It is not a problem to continue execution now, if we think this is responsible… that is you are often recommended to implement your own assert functionality.

Wouldn’t it be nice to be able to add some hints or remarks along with the location of the assert? When the assert fires and you don’t have a debugger available, this message might still tell you what the problem is:

void writeString(char const *string) {
assert(0!=string, “A null pointer was passed to writeString()!”);
...
}

The simplest solution is to just expand the assert macro and make it accept a message as well as a condition:

#ifndef NDEBUG
# define ASSERT( isOK, message ) \
if ( !(isOK) ) { \
 (void)printf(“ERROR!! Assert ‘%s’ failed on line %d “ \
  “in file ‘%s’\n%s\n”, \
     #isOK, __LINE__, __FILE__, #message); \
  __asm { int 3 } \
 }
#else
# define ASSERT( unused, message ) do {} while ( false )
#endif

Again the stringize operator helps us to stuff the message we want into the printf statement. We could call it a day now, but I am a very lazy coder… I do not want to be forced to put messages into the assert, sometimes I just want to assert.

Of course it is possible to write an ASSERT(condition) and an ASSERTm(condition, message) macro, but did I mention I am a forgetful coder too? I’d much rather have a single ASSERT statement that can do both.

The first thing that comes to mind is the fact I could do this easily with a function:

void MyAssert(bool isOK, char const *message=””) {
if ( !isOK ) {
 (void)printf(“ERROR!! Assert ‘%s’ failed on line %d “
  “in file ‘%s’\n%s\n”,
  __LINE__, __FILE__, message);
 __asm { int 3 } \
}
}

So maybe if I declared another function:

void NoAssert(bool isOK, char const *message=””) {}

And then defined assert as:

#ifndef NDEBUG
# define ASSERT MyAssert
#else
# define ASSERT NoAssert
#endif

While this seems like a quick solution, I have completely lost my extra debug information! The line information is the same now for every assert… oh… the compiler substitutes __LINE__ with the actual line number it is compiling at that moment, and since we are making a function call – all line numbers lead to the MyAssert function!

Alexandrescu demonstrates a great way around this problem [Alexandrescu]. (It is also a great article showing how you can take assertions to a higher level after this one, by making them throw exceptions!)

#ifndef NDEBUG
#define ASSERT \
struct MyAssert { \
MyAssert(bool isOK, char const *message=””) { \
 if ( !isOK ) { \
  (void)printf(“ERROR!! Assert failed in “ \
   “file ‘%s’\n%s\n”, __FILE__, message); \
   __asm { int 3 } \
  } \
 } \
} myAsserter = MyAssert
#endif

For some reason my Visual C++ 7.1 compiler will not accept the __LINE__ macro next to the __FILE__ macro in the code above. The strange thing is that the __FILE__ macro works fine, but with __LINE__ it complains:

error C2065: '__LINE__Var' : undeclared identifier

It is never easy, but I do not want to give up at this point. Since the macro is expanded as a single line into the place where we are calling it, and since the compiler apparently has no objection to me assigning __LINE__ as a default parameter in a constructor, let's try again:

#ifndef NDEBUG
#define ASSERT \
struct MyAssert { \
int mLine; \
MyAssert(int line=__LINE__) : mLine(line) {} \
MyAssert(bool isOK, char const *message=””) { \
 if ( !isOK ) { \
  (void)printf(“ERROR!! Assert failed on “ \
   “line %d in file ‘%s’\n%s\n”, \
   MyAssert().mLine, __FILE__, message); \
  __asm { int 3 } \
 } \
}\
} myAsserter = MyAssert
#endif

Now that we have our line information back, we are nearly there; as soon as we add a second assert, the compiler complains that we are redefining the struct MyAssert! If only we could keep the struct declaration local… and again Alexandrescu shows us how [Alexandrescu]:

#ifndef NDEBUG
#define ASSERT \
if ( false ) {} else struct LocalAssert { \
 int mLine; \
LocalAssert(int line=__LINE__) : mLine(line) {} \
LocalAssert(bool isOK, char const *message=””) { \
 if ( !isOK ) { \
  (void)printf(“ERROR!! Assert failed on “ \
   “line %d in file ‘%s’\n%s\n”, \
   LocalAssert().mLine, __FILE__, message); \
  __asm { int 3 } \
} \
} myAsserter = LocalAssert
#else
#define ASSERT \
if ( true ) {} else struct NoAssert { \
NoAssert(bool isOK, char const *message=””) {} \
} myAsserter = NoAssert
#endif

There is a lot of fun to be had with macros and sometimes it is possible to create the wildest incantations with them [Niebler/Alexandrescu]. I hope you are convinced that despite the fact that they can be considered evil, there is something magical about them as well.

As a final example I will show you how to create personal and customizable debug streams in the next article… all with macros.


References

[STL] – The Standard Template Library

< comes standard with your compiler but this one is very portable>

http://www.stlport.org

[BOOST] – Boost C++ Libraries

http://www.boost.org

[ACE] – The ADAPTIVE Communication Environment

http://www.cs.wustl/edu/~schmidt/ACE.html

[Niebler] – Eric Niebler

“Conditional Love: FOREACH Redux”

http://www.artima.com/cppsource/foreach.html

[Alexandrescu] – Andrei Alexandrescu

Assertions

http://www.cuj.com/documents/s=8248/cujcexp2104alexandr/


"C++" 카테고리의 다른 글
  • C++ Preprocessor: The Code in the Middle (0)2008/04/26
  • C++ Programming Tips (0)2008/04/26
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2007/07/30
  • C++ Preprocessor: The Code in the Middle (0)2007/07/30
  • Multithreading in C++ (0)2007/07/30
2007/07/30 10:34 2007/07/30 10:34
Posted by webdizen
Tags C++Keyword C++, Preprocessor
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3110

Leave your greetings.

[로그인][오픈아이디란?]

Programming/C++2007/07/30 10:33

C++ Preprocessor: The Code in the Middle

출처 : http://www.devarticles.com/c/a/cplusplu ··· iddle%2F

In this article, we examine instructions given to the preprocessor and see how they are used in general. The preprocessor handles your code before the compiler interprets it. If you have been wondering just what the preprocessor is used for, this article explains.

Whence and what art thou, execrable shape?

[John Milton 1608-1674]

The Preprocessor and the Compiler

Before the compiler interprets your code, the preprocessor handles it. Its task is to scan through your code and to look for preprocessor instructions; these can for example be used to replace certain tokens with string or numerical values, or to blank out complete sections of the code before the compiler sees it. You can recognize these instructions from the pound (#) symbol that needs to precede every preprocessor instruction.

We are going to examine these instructions and see how they are used in general. Most probably you have been using at least one of them without giving it much thought: ‘#include’. Still, this is an instruction that is handled by the preprocessor and not by the compiler. It tells it to replace the directive with the contents of the file named after it.

One of the problems a large C++ project can face is long compilation time. This can happen when ‘#include’ is used carelessly; to compile a single source, the preprocessor might (indirectly) be pasting thousands of lines of headers in front of it!

When you use third party libraries like STL-port or Boost, you will find that macros can be very useful for building different code configurations. If you have to write portable C++ code you won’t be able to live without them… however you will most probably have grown to dislike them as well.

Don’t be misled by the fact that preprocessor macros may look deceptively simple. As I’ve said before, with C++ things can become as complex as you want them to be, and to be frank, a lot of great code I have seen involved the use of macros one way or another. Maybe this is because they provide the possibility of extending the language in such a cheeky (and sneaky) way.

Let's not get ahead of ourselves. We'll first look at what the preprocessor is used for most: string substitution and conditional compilation.

The preprocessor literally changes your code (following the instructions you provide of course) and the result is a new source file, a temporary file that you cannot see but which is fed to the compiler. To specify which string tokens you want to have substituted you use the ‘#define’ directive.

For example, when you want to make use of arrays, you will have to specify how big you want one to be. By defining a MAX_STR_LEN token, you can define how big this array is once, instead of having to repeat this value throughout your code.

#define MAX_STR_LEN 80
int main(int argc, char const *argv[]) {
int anArray[MAX_STR_LEN];
for ( int idx=0; idx<MAX_STR_LEN; ++idx )
 anArray[idx]=0;
return 0;
}

It is usually not a good idea to use ‘#define’ as a substitute for constants; the preprocessor merely does a string substitution and doesn’t perform any typechecking. Depending on your debugger it can make it difficult to interpret values during a debug session.

In this case it would have been better to use a constant value:

static int const MAX_STR_LEN=80;

There are programmers that use ‘#define’ to specify constants, so be wary when you detect these. Still I think it is better to use a ‘#define’ then it is to litter your values magically throughout the code.

Imagine that the maximum string length ise shrunk to 72,.and somewhere in the code there is a for-loop that was overlooked, and would still loop until 80. An overflow has been created and worse yet, this may only become clear when the application is run in optimized mode! The loop will modify a section of the memory, causing your application to crash in a mysterious way… oh the agony when you discover the overlooked for-loop.

Sometimes string substitution can be used to help makes things clearer as well… though some consider the following deviation to be distracting:

#define EVER ; ;

for ( EVER ) { …}

When you use the preprocessor for string substitution it can do more than simply replace tokens with values. It is possible to quote strings that were passed as a parameter and you can concatenate two strings together into one.

The stringizing operator (#) substitutes the macro variable following it with its value and puts quotes around it.

For example:

#define LOG(x) std::cout << #x << std::endl

When used like:

LOG(Hello World);

will be substituted into and presented to the compiler as:

std::cout << “Hello World” << std::endl;

With the concatenation operator (##) you can tie separate strings into a new single identifier. There are many useful things you can do with this and we’ll actually need this operator when we implement customizable streams.

It is also possible to drive things to extremes. There is a company I know of that requires in its coding standard that you use their ‘Simplified Hungarian naming convention’. This means that a function that takes a const reference to an object MyClass doesn’t look like:

void foo ( MyClass const &obj );

But in their code looks like:

void foo ( rcMyClass obj );

While this might save you some typing effort, I wouldn’t recommend making such drastic changes to the C++ language. Still as an example… how did it ever get this far?

#define DECLARE_HUNGARIAN( cls ) typedef cls const & rc##cls; \
      typedef cls *       p##cls;

Now you’ve made the following code possible:

DECLARE_HUNGARIAN( MyClass )
pMyObject bar();

Macro Functions

The ‘#define’ directive is very versatile and powerful, given the simple fact that it can replace one string with another string: we can create macro functions.

#define SQUARE(x) ( (x) * (x) )

The MIN and MAX macros are also often quoted as examples:

#define MIN(x,y) ( (x) < (y) ? (x) : (y) )
#define MAX(x,y) ( (x) > (y) ? (x) : (y) )

You have to remember that all this does is string substitution, and any form of type checking is completely lacking!

An important note is that the opening parenthesis for the parameter list must immediately follow the macro name. There are no spaces allowed, since the preprocessor will otherwise consider it to be a regular string substitution.

In case you wonder why the parameters are all wrapped in parenthesis again, consider what would happen if we’d defined SQUARE as:

#define SQUARE(x) ( x * x )

Now why does the following give a result of 11 instead of 25?

int n = SQUARE( 2 + 3 );

Ouch this is not what we intended:

int n = 2 + 3 * 2 + 3;

But using parenthesis it is correct again:

int n = (2 + 3) * (2 + 3)

One more way to use ‘#define’ is to declare that a particular token is defined. This token can be used in combination with the precompiler commands “#if”, “#elif”, “#else” and “#endif” to test whether or not the preprocessor should include the code that follows the directive.

To see if a token is defined you use “#if defined” or “#elif defined” and to see if a token is not defined you use “#if !defined” or “#elif !defined”.  You can use the logical ‘&&’ and ‘||’ operators to concatenate instructions.

If for some reason you would like to remove a token from the table the preprocessor stores it in, you can undefine it with ‘#undef’.

A common use is to define a token DEBUG when compiling non-optimized code. If you use the Microsoft Developer Environment, a token _DEBUG is automatically defined for your project’s Debug configuration.

#include <stdio.h>
#define MYDEBUG
static int const MAX_STR_LEN 80;

int main(int argc, char const *argv[]) {
int anArray[MAX_STR_LEN];
for (int idx=0; idx<MAX_STR_LEN; ++idx) {
#ifdef MYDEBUG
 (void)printf(“%d “, idx);
#endif
 anArray[idx]=0;
}
}

First the precompiler runs into the ‘#include’ instruction, so it reads the ‘stdio.h’ file (which it can find using your project’s header include path) and pastes it into the temporary source file it is going to feed to the compiler. Next it encounters the ‘#define’ instruction and it stores the MYDEBUG token in a lookup table.

Finally when it comes across the ‘#ifdef’ instruction it checks whether MYDEBUG was defined in its lookup table and when this is the case it will continue to write everything it finds until the ‘#endif’ instruction into an intermediate source file.

When it cannot find the token it will check whether the next instruction is another comparison “#elif”, tells it to follow that route otherwise “#else” or that the end of the block was reached “#endif”.

If you look at the macros used to configure STL-Port (STL), Boost (BOOST) or ACE (ACE), things can become rather complex. Handy as macros can be, they do not make the code easer to read (and thus understand). Here is a snippet from the boost thread class:

class BOOST_THREAD_DECL thread : private noncopyable {
...
private:
#if defined(BOOST_HAS_WINTHREADS)
   void* m_thread;
   unsigned int m_id;
#elif defined(BOOST_HAS_PTHREADS)
private:
   pthread_t m_thread;
#elif defined(BOOST_HAS_MPTASKS)
   MPQueueID m_pJoinQueueID;
   MPTaskID m_pTaskID;
#endif
   bool m_joinable;
};

Because the macros are obfuscating which variables are needed here (because different platforms offer different facilities), it is easy to overlook that on Linux this would filter into:

private:
private:
   pthread_t m_thread;
   bool m_joinable;
};

When you want your code to be portable across different platforms, preprocessor macros will help you to maintain a single source tree. Unfortunately your eyes will have to deal with all possibilities, instead of the single one that your compiler might be choking on.

When there are so many trees it becomes difficult to see the forest.

As your application grows, you will have to organize your code among multiple source and header files. All these sources are compiled into object files, which are linked together into possibly a single executable.

Now that you are using multiple classes, many header files will have to be included into each source file. Sometimes header files will include other header files, and it might be possible that one header file will be included multiple times into a single source file.

Let’s create the ubiquitous Animal class from which we’ll derive a Hippopotamus and a Crocodile. Both classes will have to include the Animal header to be able to derive from Animal. To make things easier for whoever wants to play with a Hippopotamus and/or a Crocodile, we’ll include the Animal class in both respective header files.

Now if you are going to use them together, you’ll be including the Animal header twice. Unless you make sure it is included only once, the compiler will start complaining about multiple definitions. This is why we need inclusion guards:

#ifndef _ANIMAL_H
#define _ANIMAL_H
... // your code goes here
#endif

When the preprocessor tries to include the animal header more than once, it will find that _ANIMAL_H is already defined in its definitions table and skip the header. There is a preprocessor directive that takes care of this for the Microsoft compiler: ‘#pragma once’, but you have to remember that this makes your headers non-portable. So why not stick to inclusion guards instead?

Displaying the Intermediate Form

One of the problems with macros is that it can be very hard to deduce what the state of the preprocessor will be when it reaches a certain directive. If you’ve tried porting STL-port across different platforms, you will know what I am talking about: somewhere at the start a definition is put into the preprocessor’s table, cascading through different header files, down to the point where you are interested to see whether a piece is included in the final source that goes to the compiler or not.

Oh the hairs I would have pulled out of my head if it had not been possible to tell the compiler to show us what it sees. Start a shell and make sure you can use your compiler from the command line. If you are using the Microsoft compiler the following command will dump the output from the preprocessor :

cl /EP myfile.cpp

When you are using gcc it can be as simple as:

gcc –E myfile.cpp

Just invoke the help on the compiler to see which command line argument you’ll need. Remember, as your project grows more complex, you will have to provide the inclusion paths to headers that are not present in the same directory.

Most compilers come standard with a number of macros predefined: __DATE__ , __TIME__ , __LINE__ and __FILE__ .  These macros are very useful when you have to debug different builds. The date and time macros are translated to the date and moment the source was compiled. The line macro translates to the line number it appeared on in the source file and the file macro is translated to the actual filename of the source.

You will see that these macros come in handy when we start customizing some assert functionality.

Macro Troubles

Hopefully you are aware that macros are evil and can cause you a lot of headaches. Even though they have their clear benefits, they can cause a lot of trouble as well. There are four types of problems you can expect when using them.

Macros are not type-safe. When you find that typesafety might be an issue when you are implementing a macro function, you might want to consider replacing it with a templated function.

Macros have to be defined on one line. Things can become pretty complex and hard to read if your macro function extends over several lines. (You can make your macro span several lines by adding a backslash character (\) at the end of every line, except for the last).

Macros perform simple string substitution. This means that there is no such thing as a macro function call; the code defined in the macro is going to be pasted wherever that macro is used. This can cause considerable bloat in your source.

Because macros are expanded inline, not all debuggers can display the contents of the macro during a debug session. This means that you have no means to verify the way your macro function is being executed. This can lead to a lot of pain, only to discover you’ve forgotten a couple of parentheses!

As rough and coarse they may be, there are many nice tricks you can perform with preprocessor macros. Just take a look at the excellent crafted BOOST_FOREACH [Niebler] macro.

You will find a whole new dimension when you are able to combine macros and templates in meaningful ways. Soon I will be able to present to you the Active Object pattern and a nice macro that simplifies the implementation of this pattern using the ACE [ACE] library.

But first things first. Next time we’ll create a custom assert.


References

[STL] – The Standard Template Library

< comes standard with your compiler but this one is very portable>

http://www.stlport.org

[BOOST] – Boost C++ Libraries

http://www.boost.org

[ACE] – The ADAPTIVE Communication Environment

http://www.cs.wustl/edu/~schmidt/ACE.html

[Niebler] – Eric Niebler

“Conditional Love: FOREACH Redux”

http://www.artima.com/cppsource/foreach.html

[Alexandrescu] – Andrei Alexandrescu

Assertions

http://www.cuj.com/documents/s=8248/cujcexp2104alexandr/


"C++" 카테고리의 다른 글
  • C++ Programming Tips (0)2008/04/26
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2007/07/30
  • C++ Preprocessor: The Code in the Middle (0)2007/07/30
  • Multithreading in C++ (0)2007/07/30
  • A Simple Garbage Collector for C++ (0)2007/07/30
2007/07/30 10:33 2007/07/30 10:33
Posted by webdizen
Tags C++Keyword C++, Preprocessor
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3109

Leave your greetings.

[로그인][오픈아이디란?]

Programming/C++2007/07/30 10:29

Multithreading in C++

출처 : http://www.devarticles.com/c/a/cplusplu ··· -in-c%2F

Multithreading is growing in importance in modern programming for a variety of reasons, not the least of which being that Windows supports multithreading. While C++ does not feature built-in support for multithreading, it can be used to created multithreaded programs, which is the subject of this article. It is taken from chapter three of The Art of C++, written by Herbert Schildt (McGraw-Hill/Osborne, 2004; ISBN: 0072255129).

Multithreading is becoming an increasingly important part of modern programming. One reason for this is that multithreading enables a program to make the best use of available CPU cycles, thus allowing very efficient programs to be written. Another reason is that multithreading is a natural choice for handling event-driven code, which is so common in today’s highly distributed, networked, GUI-based environments. Of course, the fact that the most widely used operating system, Windows, supports multithreading is also a factor. Whatever the reasons, the increased use of multithreading is changing the way that programmers think about the fundamental architecture of a program. Although C++ does not contain built-in support for multithreaded programs, it is right at home in this arena.

Because of its growing importance, this chapter explores using C++ to create multithreaded programs. It does so by developing two multithreaded applications. The first is a thread control panel, which you can use to control the execution of threads within a program. This is both an interesting demonstration of multithreading and a practical tool that you can use when developing multithreaded applications. The second example shows how to apply multithreading to a practical example by creating a modified version of the garbage collector from Chapter 2 that runs in a background thread.

This chapter also serves another purpose: it shows how adept C++ is at interfacing directly to the operating system. In some other languages, such as Java, there is a layer of processing between your program and the OS. This layer adds overhead that can be unacceptable for some types of programs, such as those used in a real-time environment. In sharp contrast, C++ has direct access to low-level functionality provided by the operating system. This is one of the reasons C++ can produce higher performance code.

What Is Multithreading?

Before beginning, it is necessary to define precisely what is meant by the term multithreading. Multithreading is a specialized form of multitasking. In general, there are two types of multitasking: process-based and thread-based. A process is, in essence, a program that is executing. Thus, process-based multitasking is the feature that allows your computer to run two or more programs concurrently. For example, it is process-based multitasking that allows you to run a word processor at the same time you are using a spreadsheet or browsing the Internet. In process-based multitasking, a program is the smallest unit of code that can be dispatched by the scheduler.

A thread is a dispatchable unit of executable code. The name comes from the concept of a “thread of execution.” In a thread-based multitasking environment, all processes have at least one thread, but they can have more. This means that a single program can perform two or more tasks concurrently. For instance, a text editor can be formatting text at the same time that it is printing, as long as these two actions are being performed by two separate threads. The differences between process-based and thread-based multitasking can be summarized like this: Process-based multitasking handles the concurrent execution of programs. Thread-based multitasking deals with the concurrent execution of pieces of the same program.

In the preceding discussions, it is important to clarify that true concurrent execution is possible only in a multiple-CPU system in which each process or thread has unrestricted access to a CPU. For single CPU systems, which constitute the vast majority of systems in use today, only the appearance of simultaneous execution is achieved. In a single CPU system, each process or thread receives a portion of the CPU’s time, with the amount of time determined by several factors, including the priority of the process or thread. Although truly concurrent execution does not exist on most computers, when writing multithreaded programs, you should assume that it does. This is because you can’t know the precise order in which separate threads will be executed, or if they will execute in the same sequence twice. Thus, its best to program as if true concurrent execution is the case.

Multithreading Changes the Architecture of a Program

Multithreading changes the fundamental architecture of a program. Unlike a single-threaded program that executes in a strictly linear fashion, a multithreaded program executes portions of itself concurrently. Thus, all multithreaded programs include an element of parallelism. Consequently, a major issue in multithreaded programs is managing the interaction of the threads.

As explained earlier, all processes have at least one thread of execution, which is called the main thread. The main thread is created when your program begins. In a multithreaded program, the main thread creates one or more child threads. Thus, each multithreaded process starts with one thread of execution and then creates one or more additional threads. In a properly designed program, each thread represents a single logical unit of activity.

The principal advantage of multithreading is that it enables you to write very efficient programs because it lets you utilize the idle time that is present in most programs. Most I/O devices, whether they are network ports, disk drives, or the keyboard, are much slower than the CPU. Often, a program will spend a majority of its execution time waiting to send or receive data. With the careful use of multithreading, your program can execute another task during this idle time. For example, while one part of your program is sending a file over the Internet, another part can be reading keyboard input, and still another can be buffering the next block of data to send.

Why Doesn’t C++ Contain Built-In Support for Multithreading?

C++ does not contain any built-in support for multithreaded applications. Instead, it relies entirely upon the operating system to provide this feature. Given that both Java and C# provide built-in support for multithreading, it is natural to ask why this isn’t also the case for C++. The answers are efficiency, control, and the range of applications to which C++ is applied. Let’s examine each.

By not building in support for multithreading, C++ does not attempt to define a “one size fits all” solution. Instead, C++ allows you to directly utilize the multithreading features provided by the operating system. This approach means that your programs can be multithreaded in the most efficient means supported by the execution environment. Because many multitasking environments offer rich support for multithreading, being able to access that support is crucial to the creation of high-performance, multithreaded programs.

Using operating system functions to support multithreading gives you access to the full range of control offered by the execution environment. Consider Windows. It defines a rich set of thread-related functions that enable finely grained control over the creation and management of a thread. For example, Windows has several ways to control access to a shared resource, including semaphores, mutexes, event objects, waitable timers, and critical sections. This level of flexibility cannot be easily designed into a language because the capabilities of operating systems differ. Thus, language-level support for multithreading usually means offering only a “lowest common denominator” of features. With C++, you gain access to all the features that the operating system provides. This is a major advantage when writing high-performance code.

C++ was designed for all types of programming, from embedded systems in which there is no operating system in the execution environment to highly distributed, GUI-based end-user applications and everything in between. Therefore, C++ cannot place significant constraints on its execution environment. Building in support for multithreading would have inherently limited C++ to only those environments that supported it and thus prevented C++ from being used to create software for nonthreaded environments.

In the final analysis, not building in support of multithreading is a major advantage for C++ because it enables programs to be written in the most efficient way possible for the target execution environment. Remember, C++ is all about power. In the case of multithreading, it is definitely a situation in which “less is more.”

What Operating System and Compiler?

Because C++ relies on the operating system to provide support for multithreaded programming, it is necessary to choose an operating system as the target for the multithreaded applications in this chapter. Because Windows is the most widely used operating system in the world, it is the operating system used in this chapter. However, much of the information can be generalized to any OS that supports multithreading.

Because Visual C++ is arguably the most widely used compiler for producing Windows programs, it is the compiler required by the examples in this chapter. The importance of this is made apparent in the following section. However, if you are using another compiler, the code can be easily adapted to accommodate it.

NOTE


The examples in this chapter assume a basic, working knowledge of Windows programming.

Windows offers a wide array of Application Programming Interface (API) functions that support multithreading. Many readers will be at least somewhat familiar with the multithreading functions offered by Windows, but for those who are not, an overview of those used in this chapter is presented here. Keep in mind that Windows provides many other multithreading-based functions that you might want to explore on your own.

To use Windows’ multithreading functions, you must include <windows.h> in your program.

Creating and Terminating a Thread

To create a thread, the Windows API supplies the CreateThread( ) function. Its prototype is shown here:

  HANDLE CreateThread(LPSECURITY_ATTRIBUTES secAttr,
                            SIZE_T stackSize,
                           
LPTHREAD_START_ROUTINE threadFunc,
                           
LPVOID param,
                           
DWORD flags,
                           
LPDWORD threadID);

Here, secAttr is a pointer to a set of security attributes pertaining to the thread. However, if secAttr is NULL, then the default security descriptor is used.

Each thread has its own stack. You can specify the size of the new thread’s stack in bytes using the stackSize parameter. If this integer value is zero, then the thread will be given a stack that is the same size as the creating thread. In this case, the stack will be expanded, if necessary. (Specifying zero is the common approach taken to thread stack size.)

Each thread of execution begins with a call to a function, called the thread function, within the creating process. Execution of the thread continues until the thread function returns. The address of this function (that is, the entry point to the thread) is specified in threadFunc. All thread functions must have this prototype:

  DWORD WINAPI threadfunc(LPVOID param);

Any argument that you need to pass to the new thread is specified in CreateThread( )’s param. This 32-bit value is received by the thread function in its parameter. This parameter may be used for any purpose. The function returns its exit status.

The flags parameter determines the execution state of the thread. If it is zero, the thread begins execution immediately. If it is CREATE_SUSPEND, the thread is created in a suspended state, awaiting execution. (It may be started using a call to ResumeThread( ), discussed later.)

The identifier associated with a thread is returned in the long integer pointed to by threadID.

The function returns a handle to the thread if successful or NULL if a failure occurs. The thread handle can be explicitly destroyed by calling CloseHandle( ). Otherwise, it will be destroyed automatically when the parent process ends.

As just explained, a thread of execution terminates when its entry function returns. The process may also terminate the thread manually, using either TerminateThread( ) or ExitThread( ), whose prototypes are shown here:

  BOOL TerminateThread(HANDLE thread, DWORD status);
  VOID ExitThread(DWORD status);

For TerminateThread( ), thread is the handle of the thread to be terminated. ExitThread( ) can only be used to terminate the thread that calls ExitThread( ). For both functions, status is the termination status. TerminateThread( ) returns nonzero if successful and zero otherwise.

Calling ExitThread( ) is functionally equivalent to allowing a thread function to return normally. This means that the stack is properly reset. When a thread is terminated using TerminateThread( ), it is stopped immediately and does not perform any special cleanup activities. Also, TerminateThread( ) may stop a thread during an important operation. For these reasons, it is usually best (and easiest) to let a thread terminate normally when its entry function returns.

The Visual C++ Alternatives to CreateThread( ) and ExitThread( )

Although CreateThread( ) and ExitThread( ) are the Windows API functions used to create and terminate a thread, we won’t be using them in this chapter! The reason is that when these functions are used with Visual C++ (and possibly other Windows-compatible compilers), they can result in memory leaks, the loss of a small amount of memory. For Visual C++, if a multithreaded program utilizes C/C++ standard library functions and uses CreateThread( ) and ExitThread( ), then small amounts of memory are lost. (If your program does not use the C/C++ standard library, then no such losses will occur.) To eliminate this problem, you must use functions defined by the C/C++ runtime library to start and stop threads rather than those specified by the Win32 API. These functions parallel CreateThread( ) and ExitThread( ), but do not generate a memory leak.

Note


If you are using a compiler other than Visual C++,  check its documentation to determine if you need to bypass CreateThread( ) and ExitThread( ) and how to do so, if necessary.

The Visual C++ alternatives to CreateThread( ) and ExitThread( ) are _beginthreadex( ) and _endthreadex( ). Both require the header file <process.h>. Here is the prototype for _beginthreadex( ):

  uintptr_t _beginthreadex(void *secAttr, unsigned stackSize,
                                  unsigned (__stdcall *threadFunc)(void *),
                                  void *param, unsigned flags,
                                  unsigned *threadID);

As you can see, the parameters to _beginthreadex( ) parallel those to CreateThread( ). Furthermore, they have the same meaning as those specified by CreateThread( ). secAttr is a pointer to a set of security attributes pertaining to the thread. However, if secAttr is NULL, then the default security descriptor is used. The size of the new thread’s stack, in bytes, is passed in stackSize parameter. If this value is zero, then the thread will be given a stack that is the same size as the main thread of the process that creates it.

The address of the thread function (that is, the entry point to the thread) is specified in threadFunc. For _beginthreadex( ), a thread function must have this prototype:

  unsigned __stdcall threadfunc(void * param);

This prototype is functionally equivalent to the one for CreateThread( ), but it uses different type names. Any argument that you need to pass to the new thread is specified in the param parameter.

The flags parameter determines the execution state of the thread. If it is zero, the thread begins execution immediately. If it is CREATE_SUSPEND, the thread is created in a suspended state, awaiting execution. (It may be started using a call to ResumeThread( ).) The identifier associated with a thread is returned in the double word pointed to by threadID.

The function returns a handle to the thread if successful or zero if a failure occurs. The type uintptr_t specifies a Visual C++ type capable of holding a pointer or handle.

The prototype for _endthreadex( ) is shown here:

  void _endthreadex(unsigned status);

It functions just like ExitThread( ) by stopping the thread and returning the exit code specified in status.

Because the most widely used compiler for Windows is Visual C++, the examples in this chapter will use _beginthreadex( ) and _endthreadex( ) rather their equivalent API functions. If you are using a compiler other than Visual C++, simply substitute CreateThread( ) and EndThread( ).

When using _beginthreadex( ) and _endthreadex( ), you must remember to link in the multithreaded library. This will vary from compiler to compiler. Here are some examples. When using the Visual C++ command-line compiler, include the –MT option. To use the multithreaded library from the Visual C++ 6 IDE, first activate the Project | Settings property sheet. Then, select the C/C++ tab. Next, select Code Generation from the Category list box and then choose Multithreaded in the Use Runtime Library list box. For Visual C++ 7 .NET IDE, select Project | Properties. Next, select the C/C++ entry and highlight Code Generation. Finally, choose Multi-threaded as the runtime library.

Suspending and Resuming a Thread

A thread of execution can be suspended by calling SuspendThread( ). It can be resumed by calling ResumeThread( ). The prototypes for these functions are shown here:

  DWORD SuspendThread(HANDLE hThread);
  DWORD ResumeThread(HANDLE hThread);

For both functions, the handle to the thread is passed in hThread.

Each thread of execution has associated with it a suspend count. If this count is zero, then the thread is not suspended. If it is nonzero, the thread is in a suspended state. Each call to SuspendThread( ) increments the suspend count. Each call to ResumeThread( ) decrements the suspend count. A suspended thread will resume only when its suspend count has reached zero. Therefore, to resume a suspended thread implies that there must be the same number of calls to ResumeThread( ) as there have been calls to SuspendThread( ).

Both functions return the thread’s previous suspend count or –1 if an error occurs.

Changing the Priority of a Thread

In Windows, each thread has associated with it a priority setting. A thread’s priority determines how much CPU time a thread receives. Low priority threads receive little time. High priority threads receive a lot. Of course, how much CPU time a thread receives has a profound impact on its execution characteristics and its interaction with other threads currently executing in the system.

In Windows, a thread’s priority setting is the combination of two values: the overall priority class of the process and the priority setting of the individual thread relative to that priority class. That is, a thread’s actual priority is determined by combining the process’s priority class with the thread’s individual priority level. Each is examined next.

By default, a process is given a priority class of normal, and most programs remain in the normal priority class throughout their execution lifetime. Although neither of the examples in this chapter changes the priority class, a brief overview of the thread priority classes is given here in the interest of completeness.

Windows defines six priority classes, which correspond to the value shown here, in order of highest to lowest priority:

REALTIME_PRIORITY_CLASS

HIGH_PRIORITY_CLASS

ABOVE_NORMAL_PRIORITY_CLASS

NORMAL_PRIORITY_CLASS

BELOW_NORMAL_PRIORITY_CLASS

IDLE_PRIORITY_CLASS

Programs are given the NORMAL_PRIORITY_CLASS by default. Usually, you won’t need to alter the priority class of your program. In fact, changing a process’ priority class can have negative consequences on the overall performance of the computer system. For example, if you increase a program’s priority class to REALTIME_PRIORITY_CLASS, it will dominate the CPU. For some specialized applications, you may need to increase an application’s priority class, but usually you won’t. As mentioned, neither of the applications in this chapter changes the priority class.

In the event that you do want to change the priority class of a program, you can do by calling SetPriorityClass( ). You can obtain the current priority class by calling GetPriorityClass( ). The prototypes for these functions are shown here:

  DWORD GetPriorityClass(HANDLE hApp);
  BOOL SetPriorityClass(HANDLE hApp, DWORD priority);

Here, hApp is the handle of the process. GetPriorityClass( ) returns the priority class of the application or zero on failure. For SetPriorityClass( ), priority specifies the process’s new priority class.

Thread Priorities

For any given priority class, each individual thread’s priority determines how much CPU time it receives within its process. When a thread is first created, it is given normal priority, but you can change a thread’s priority—even while it is executing.

You can obtain a thread’s priority setting by calling GetThreadPriority( ). You can increase or decrease a thread’s priority using SetThreadPriority( ). The prototypes for these functions are shown here:

  BOOL SetThreadPriority(HANDLE hThread, int priority);
  int GetThreadPriority(HANDLE hThread);

For both functions, hThread is the handle of the thread. For SetThreadPriority( ), priority is the new priority setting. If an error occurs, SetThreadPriority( ) returns zero. It returns nonzero otherwise. For GetThreadPriority( ), the current priority setting is returned. The priority settings are shown here, in order of highest to lowest:

Thread Priority

Value

THREAD_PRIORITY_TIME_CRITICAL

15

THREAD_PRIORITY_HIGHEST

2

THREAD_PRIORITY_ABOVE_NORMAL

1

THREAD_PRIORITY_NORMAL

0

THREAD_PRIORITY_BELOW_NORMAL

-1

THREAD_PRIORITY LOWEST

-2

THREAD_PRIORITY_IDLE

-15

These values are increments or decrements that are applied relative to the priority class of the process. Through the combination of a process’ priority class and thread priority, Windows supports 31 different priority settings for application programs.

GetThreadPriority( ) returns THREAD_PRIORITY_ERROR_RETURN if an error occurs.

For the most part, if a thread has the NORMAL_PRIORITY class, you can freely experiment with changing its priority setting without fear of catastrophically affecting overall system performance. As you will see, the thread control panel developed in the next section allows you to alter the priority setting of a thread within a process (but does not change its priority class).

Obtaining the Handle of the Main Thread

It is possible to control the execution of the main thread. To do so, you will need to acquire its handle. The easiest way to do this is to call GetCurrentThread( ), whose prototype is shown here:

  HANDLE GetCurrentThread(void);

This function returns a pseudohandle to the current thread. It is called a pseudohandle because it is a predefined value that always refers to the current thread rather than specifically to the calling thread. It can, however, be used any place that a normal thread handle can.

Synchronization

When using multiple threads or processes, it is sometimes necessary to coordinate the activities of two or more. This process is called synchronization. The most common use of synchronization occurs when two or more threads need access to a shared resource that must be used by only one thread at a time. For example, when one thread is writing to a file, a second thread must be prevented from doing so at the same time. Another reason for synchronization is when one thread is waiting for an event that is caused by another thread. In this case, there must be some means by which the first thread is held in a suspended state until the event has occurred. Then the waiting thread must resume execution.

There are two general states that a task may be in. First, it may be executing (or ready to execute as soon as it obtains its time slice). Second, a task may be blocked, awaiting some resource or event, in which case its execution is suspended until the needed resource is available or the event occurs.

If you are not familiar with the synchronization problem or its most common solution, the semaphore, the next section discusses it.

Understanding the Synchronization Problem

Windows must provide special services that allow access to a shared resource to be synchronized, because without help from the operating system, there is no way for one process or thread to know that it has sole access to a resource. To understand this, imagine that you are writing programs for a multitasking operating system that does not provide any synchronization support. Further imagine that you have two concurrently executing threads, A and B, both of which, from time to time, require access to some resource R (such as a disk file) that must be accessed by only one thread at a time. As a means of preventing one thread from accessing R while the other is using it, you try the following solution. First, you establish a variable called flag that is initialized to zero and can be accessed by both threads. Then, before using each piece of code that accesses R, you wait for flag to be cleared, then set flag, access R, and finally, clear flag. That is, before either thread accesses R, it executes this piece of code:

while(flag) ; // wait for flag to be cleared
flag = 1; // set flag
// ... access resource R ...
flag = 0; // clear the flag

The idea behind this code is that neither thread will access R if flag is set. Conceptually, this approach is in the spirit of the correct solution. However, in actual fact it leaves much to be desired for one simple reason: it won’t always work! Let’s see why.

Using the code just given, it is possible for both processes to access R at the same time. The while loop is, in essence, performing repeated load and compare instructions on flag or, in other words, it is testing flag’s value. When flag is cleared, the next line of code sets flag’s value. The trouble is that it is possible for these two operations to be performed in two different time slices. Between the two time slices, the value of flag might have been accessed by the other thread, thus allowing R to be used by both threads at the same time. To understand this, imagine that thread A enters the while loop and finds that flag is zero, which is the green light to access R. However, before it can set flag to 1, its time slice expires and thread B resumes execution. If B executes its while, it too will find that flag is not set and assume that it is safe to access R. However, when A resumes it will also begin accessing R. The crucial aspect of the problem is that the testing and setting of flag do not comprise one uninterruptible operation. Rather, as just illustrated, they can be separated by a time slice. No matter how you try, there is no way, using only application-level code, that you can absolutely guarantee that one and only one thread will access R at one time.

The solution to the synchronization problem is as elegant as it is simple. The operating system (in this case Windows) provides a routine that in one uninterrupted operation, tests and, if possible, sets a flag. In the language of operating systems engineers, this is called a test and set operation. For historical reasons, the flags used to control access to a shared resource and provide synchronization between threads (and processes) are called semaphores. The semaphore is at the core of the Windows synchronization system.

Windows supports several types of synchronization objects. The first type is the classic semaphore. When using a semaphore, a resource can be completely synchronized, in which case one and only one thread or process can access it at any one time, or the semaphore can allow no more than a small number of processes or threads access at any one time. Semaphores are implemented using a counter that is decremented when a task is granted the semaphore and incremented when the task releases it.

The second synchronization object is the mutex semaphore, or just mutex, for short. A mutex synchronizes a resource such that one and only one thread or process can access it at any one time. In essence, a mutex is a special case version of a standard semaphore.

The third synchronization object is the event object. It can be used to block access to a resource until some other thread or process signals that it can be used. (That is, an event object signals that a specified event has occurred.)

The fourth synchronization object is the waitable timer. A waitable timer blocks a thread’s execution until a specific time. You can also create timer queues, which are lists of timers.

You can prevent a section of code from being used by more than one thread at a time by making it into a critical section using a critical section object. Once a critical section is entered by one thread, no other thread may use it until the first thread has left the critical section.

The only synchronization object used in this chapter is the mutex, which is described in the following section. However, all synchronization objects defined by Windows are available to the C++ programmer. As explained, this is one of the major advantages that results from C++’s reliance on the operating system to handle multithreading: all multithreading features are at your command.

Using a Mutex to Synchronize Threads

As explained, a mutex is a special-case semaphore that allows only one thread to access a resource at any given time. Before you can use a mutex, you must create one using CreateMutex( ), whose prototype is shown here:

HANDLE CreateMutex(LPSECURITY_ATTRIBUTES secAttr,
BOOL acquire,
LPCSTR name);

Here, secAttr is a pointer to the security attributes. If secAttr is NULL, the default security descriptor is used.

If the creating thread desires control of the mutex, then acquire must be true. Otherwise, pass false.

The name parameter points to a string that becomes the name of the mutex object. Mutexes are global objects, which may be used by other processes. As such, when two processes each open a mutex using the same name, both are referring to the same mutex. In this way, two processes can be synchronized. The name may also be NULL, in which case the semaphore is localized to one process.

The CreateMutex( ) function returns a handle to the semaphore if successful or NULL on failure. A mutex handle is automatically closed when the main process ends. You can explicitly close a mutex handle when it is no longer needed by calling CloseHandle( ).

Once you have created a semaphore, you use it by calling two related functions: WaitForSingleObject( ) and ReleaseMutex( ). The prototypes for these functions are shown here:

  DWORD WaitForSingleObject(HANDLE hObject, DWORD howLong);
  BOOL ReleaseMutex(HANDLE hMutex);

WaitForSingleObject( ) waits on a synchronization object. It does not return until the object becomes available or a time-out occurs. For use with mutexes, hObject will be the handle of a mutex. The howLong parameter specifies, in milliseconds, how long the calling routine will wait. Once that time has elapsed, a time-out error will be returned. To wait indefinitely, use the value INFINITE. The function returns WAIT_OBJECT_0 when successful (that is, when access is granted). It returns WAIT_TIMEOUT when time-out is reached.

ReleaseMutex( ) releases the mutex and allows another thread to acquire it. Here, hMutex is the handle to the mutex. The function returns nonzero if successful and zero on failure.

To use a mutex to control access to a shared resource, wrap the code that accesses that resource between a call to WaitForSingleObject( ) and ReleaseMutex( ), as shown in this skeleton. (Of course, the time-out period will differ from application to application.)

if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT) {
  // handle time-out error
}
// access the resource

ReleaseMutex(hMutex);

Generally, you will want to choose a time-out period that will be more than enough to accommodate the actions of your program. If you get repeated time-out errors when developing a multithreaded application, it usually means that you have created a deadlock condition. Deadlock occurs when one thread is waiting on a mutex that another thread never releases.

Creating a Thread Control Panel

When developing multithreaded programs, it is often useful to experiment with various priority settings. It is also useful to be able to dynamically suspend and resume a thread, or even terminate a thread. As you will see, it is quite easy, using the thread functions just described, to create a thread control panel that allows you to accomplish these things. Further, you can use the control panel while your multithreaded program is running. The dynamic nature of the thread control panel allows you to easily change the execution profile of a thread and observe the results.

The thread control panel developed in this section is capable of controlling one thread. However, you can create as many panels as needed, with each controlling a different thread. For the sake of simplicity, the control panel is implemented as a modeless dialog box that is owned by the desktop, not the application whose thread it controls.

The thread control panel is capable of performing the following actions:

  • Setting a thread’s priority
  • Suspending a thread
  • Resuming a thread
  • Terminating a thread

It also displays the current priority setting of the thread. The thread control dialog box is shown in Figure 3-1.

As stated, the control panel is as a modeless dialog box. As you know, when a modeless dialog box is activated, the rest of the application is still active. Thus, the control panel runs independently of the application for which it is being used.


Figure 3-1.  The Thread Control dialog box

The code for the thread control panel is shown here. This file is called tcp.cpp.

// A thread control panel.
#include <map>
#include <windows.h>
#include "panel.h"
using namespace std;
const int NUMPRIORITIES = 5;
const int OFFSET = 2;
// Array of strings for priority list box.
char priorities[NUMPRIORITIES][80] = {
  "Lowest",
  "Below Normal",
  "Normal",
  "Above Normal",
  "Highest"
};
// A Thread Control Panel Class.
class ThrdCtrlPanel {
  // Information about the thread under control.
  struct ThreadInfo {
   HANDLE hThread; //  handle of thread
   int priority;   //  current priority
   bool suspended; //  true if suspended
   ThreadInfo(HANDLE ht, int p, bool s) {
     hThread = ht;
     priority = p;
     suspended = s;
   }
  };
  // This map holds a ThreadInfo for each
  // active thread control panel.
  static map<HWND, ThreadInfo> dialogmap;
public:
  // Construct a control panel.
  ThrdCtrlPanel(HINSTANCE hInst, HANDLE hThrd);
  // The control panel's callback function.
  static LRESULT CALLBACK ThreadPanel(HWND hwnd, UINT message,
                        WPARAM wParam, LPARAM lParam);
};
// Define static member dialogmap.
map<HWND, ThrdCtrlPanel::ThreadInfo>
  ThrdCtrlPanel::dialogmap;
// Create a thread control panel. ThrdCtrlPanel::ThrdCtrlPanel(HINSTANCE hInst,
                           HANDLE hThrd)
{
  ThreadInfo ti(hThrd,
               GetThreadPriority(hThrd)+OFFSET,
               false);
  // Owner window is desktop.
  HWND hDialog = CreateDialog(hInst, "ThreadPanelDB",
                     NULL,
                     (DLGPROC) ThreadPanel);
  // Put info about this dialog box in the map.
  dialogmap.insert(pair<HWND, ThreadInfo>(hDialog, ti));
  // Set the control panel's title.
  char str[80] = "Control Panel for Thread ";
  char str2[4];
  _itoa(dialogmap.size(), str2, 10);
  strcat(str, str2);
  SetWindowText(hDialog, str);
  // Offset each dialog box instance.
  MoveWindow(hDialog, 30*dialogmap.size(),
          30*dialogmap.size(),
          300, 250, 1);
  // Update priority setting in the list box.
  SendDlgItemMessage(hDialog, IDD_LB, LB_SETCURSEL,
                       (WPARAM) ti.priority, 0);
  // Increase priority to ensure control. You can
  // change or remove this statement based on your
  // execution environment.
  SetThreadPriority(GetCurrentThread(),
                  THREAD_PRIORITY_ABOVE_NORMAL);
}
// Thread control panel dialog box callback function.
LRESULT CALLBACK ThrdCtrlPanel::ThreadPanel(HWND hwnd,
                                     UINT message,
                                     WPARAM wParam,
                                     LPARAM lParam)
{
  int i;
  HWND hpbRes, hpbSus, hpbTerm;
  switch(message) {
   case WM_INITDIALOG:
     // Initialize priority list box.
     for(i=0; i<NUMPRIORITIES i++) {
       SendDlgItemMessage(hwnd, IDD_LB,
           LB_ADDSTRING, 0, (LPARAM) priorities[i]);
       }
       // Set suspend and resume buttons for thread.
       hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
       hpbRes = GetDlgItem(hwnd, IDD_RESUME);
       EnableWindow(hpbSus, true);  // enable Suspend
       EnableWindow(hpbRes, false); // disable Resume
       return 1;
   case WM_COMMAND:
     map<HWND, ThreadInfo>::iterator p = dialogmap.find(hwnd);
   switch(LOWORD(wParam)) {
     case IDD_TERMINATE:
       TerminateThread(p->second.hThread, 0);
       // Disable Terminate button.
       hpbTerm = GetDlgItem(hwnd, IDD_TERMINATE); }
       EnableWindow(hpbTerm, false); // disable
       // Disable Suspend and Resume buttons.
       hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
       hpbRes = GetDlgItem(hwnd, IDD_RESUME);
       EnableWindow(hpbSus, false); // disable Suspend
       EnableWindow(hpbRes, false); // disable Resume
       return 1;
     case IDD_SUSPEND:
       SuspendThread(p->second.hThread);
       // Set state of the Suspend and Resume buttons.
       hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
       hpbRes = GetDlgItem(hwnd, IDD_RESUME);
       EnableWindow(hpbSus, false); // disable Suspend
       EnableWindow(hpbRes, true);  // enable Resume
       p->second.suspended = true;
       return 1;
     case IDD_RESUME:
       ResumeThread(p->second.hThread);
       // Set state of the Suspend and Resume buttons.
       hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
       hpbRes = GetDlgItem(hwnd, IDD_RESUME); /'
       EnableWindow(hpbSus, true);  // enable Suspend /'
       EnableWindow(hpbRes, false); // disable Resume
       p->second.suspended = false;
       return 1;
     case IDD_LB:
       // If a list box entry was clicked,
       // then change the priority.
       if(HIWORD(wParam)==LBN_DBLCLK) {
        p->second.priority = SendDlgItemMessage(hwnd,
                             IDD_LB, LB_GETCURSEL,/'
                             0, 0);
       SetThreadPriority(p->second.hThread,
                                   p->second.priority-OFFSET);
       }
       return 1;
     case IDCANCEL:
       // If thread is suspended when panel is closed,
       // then resume thread to prevent deadlock.
       if(p->second.suspended) {
          ResumeThread(p->second.hThread);
         p->second.suspended = false;
       }
       // Remove this thread from the list.
       dialogmap.erase(hwnd);
       // Close the panel.
       DestroyWindow(hwnd);?
       return 1;
       }
 }
  return 0;
}

The control panel requires the following resource file, called tcp.rc:

#include <windows.h>
#include "panel.h"
ThreadPanelDB DIALOGEX 20, 20, 140, 110
CAPTION "Thread Control Panel"
STYLE WS_BORDER | WS_VISIBLE | WS_POPUP | WS_CAPTION | WS_SYSMENU
{
  DEFPUSHBUTTON "Done", IDCANCEL, 55, 80, 33, 14
  PUSHBUTTON "Terminate", IDD_TERMINATE, 10, 20, 42, 12
  PUSHBUTTON "Suspend", IDD_SUSPEND, 10, 35, 42, 12
  PUSHBUTTON "Resume", IDD_RESUME, 10, 50, 42, 12
  LISTBOX IDD_LB, 65, 20, 63, 42, LBS_NOTIFY | WS_VISIBLE |
         WS_BORDER | WS_VSCROLL | WS_TABSTOP
  CTEXT "Thread Priority", IDD_TEXT1, 65, 8, 64, 10
  CTEXT "Change State", IDD_TEXT2, 0, 8, 64, 10
}

The control panel uses the following header file called panel.h:

#define IDD_LB           200
#define IDD_TERMINATE    202
#define IDD_SUSPEND      204
#define IDD_RESUME       206
#define IDD_TEXT1        208
#define IDD_TEXT2        209

To use the thread control panel, follow these steps:

  1. Include tcp.cpp in your program.
  2. Include tcp.rc in your program’s resource file.
  3. Create the thread or threads that you want to control.
  4. Instantiate a ThrdCtrlPanel object for each thread.

Each ThrdCtrlPanel object links a thread with a dialog box that controls it. For large projects in which multiple files need access to ThrdCtrlPanel, you will need to use a header file called tcp.h that contains the declaration for ThrdCtrlPanel. Here is tcp.h:

// A header file for the ThrdCtrlPanel class.
class ThrdCtrlPanel {
public:
  // Construct a control panel.
  ThrdCtrlPanel(HINSTANCE hInst, HANDLE hThrd);
  // The control panel's callback function.
  static LRESULT CALLBACK ThreadPanel(HWND hwnd, UINT message,
                        WPARAM wParam, LPARAM lParam);
};

Let’s take a closer look at the thread control panel. It begins by defining the following global definitions:

const int NUMPRIORITIES = 5;
const int OFFSET = 2;
// Array of strings for priority list box.
char priorities[NUMPRIORITIES][80] = {
  "Lowest",
  "Below Normal",
  "Normal",
  "Above Normal",
  "Highest"
};

The priorities array holds strings that correspond to a thread’s priority setting. It initializes the list box inside the control panel that displays the current thread priority. The number of priorities is specified by NUMPRIORITIES, which is 5 for Windows. Thus, NUMPRIORITIES defines the number of different priorities that a thread may have. (If you adapt the code for use with another operating system, a different value might be required.) Using the control panel, you can set a thread to one of the following priorities:

  THREAD_PRIORITY_HIGHEST

  THREAD_PRIORITY_ABOVE_NORMAL

  THREAD_PRIORITY_NORMAL

  THREAD_PRIORITY_BELOW_NORMAL

  THREAD_PRIORITY_LOWEST

The other two thread priority settings:

  THREAD_PRIORITY_TIME_CRITICAL

  THREAD_PRIORITY_IDLE

are not supported because, relative to the control panel, they are of little practical value. For example, if you want to create a time-critical application, you are better off making its priority class time-critical.

OFFSET defines an offset that will be used to translate between list box indexes and thread priorities. You should recall that normal priority has the value zero. In this example, the highest priority is THREAD_PRIORITY_HIGHEST, which is 2. The lowest priority is THREAD_PRIORITY_LOWEST, which is –2. Because list box indexes begin at zero, the offset is used to convert between indexes and priority settings.

Next, the ThrdCtrlPanel class is declared. It begins as shown here:

// A Thread Control Panel Class.
class ThrdCtrlPanel {
  // Information about the thread under control.
  struct ThreadInfo {
   HANDLE hThread; // handle of thread
   int priority;   // current priority
   bool suspended; // true if suspended
   ThreadInfo(HANDLE ht, int p, bool s) {
     hThread = ht;
     priority = p;
     suspended = s;
   }
  };
  // This map holds a ThreadInfo for each
  // active thread control panel.
  static map<HWND, ThreadInfo> dialogmap;

Information about the thread under control is contained within a structure of type ThreadInfo. The handle of the thread is stored in hThread. Its priority is stored in priority. If the thread is suspended, then suspended will be true. Otherwise, suspended will be false.

The static member dialogmap is an STL map that links the thread information with the handle of the dialog box used to control that thread. Because there can be more than one thread control panel active at any given time, there must be some way to determine which thread is associated with which panel. It is dialogmap that provides this linkage.

The ThreadCtrlPanel Constructor

The ThrdCtrlPanel constructor is shown here. The constructor is passed the instance handle of the application and the handle of the thread being controlled. The instance handle is needed to create the control panel dialog box.

// Create a thread control panel. ThrdCtrlPanel::ThrdCtrlPanel(HINSTANCE hInst,
                            HANDLE hThrd)
{
  ThreadInfo ti(hThrd,
               GetThreadPriority(hThrd)+OFFSET,
               false);
  // Owner window is desktop.
  HWND hDialog = CreateDialog(hInst, "ThreadPanelDB",
                      NULL,
                     (DLGPROC) ThreadPanel);
  // Put info about this dialog box in the map.
  dialogmap.insert(pair<HWND, ThreadInfo>(hDialog, ti));
  // Set the control panel's title.
  char str[80] = "Control Panel for Thread ";
  char str2[4];
  _itoa(dialogmap.size(), str2, 10);
  strcat(str, str2);
  SetWindowText(hDialog, str);
  // Offset each dialog box instance.
  MoveWindow(hDialog, 30*dialogmap.size(),
            30*dialogmap.size(),
            300, 250, 1);
  // Update priority setting in the list box.
  SendDlgItemMessage(hDialog, IDD_LB, LB_SETCURSEL,
                         (WPARAM) ti.priority, 0);
  // Increase priority to ensure control. You can
  // change or remove this statement based on your
  // execution environment.
  SetThreadPriority(GetCurrentThread(),
                   THREAD_PRIORITY_ABOVE_NORMAL);
}

The constructor begins by creating a ThreadInfo instance called ti that contains the initial settings for the thread. Notice that the priority is obtained by calling GetThreadPriority( ) for the thread being controlled. Next, the control panel dialog box is created by calling CreateDialog( ). CreateDialog( ) is a Windows API function that creates a modeless dialog box, which makes it independent of the application that creates it. The handle of this dialog box is returned and stored in hDialog. Next, hDialog and the thread information contained in ti are stored in dialogmap. Thus, the thread is linked with the dialog box that controls it.

Next, the title of the dialog box is set to reflect the number of the thread. The number of the thread is obtained based on the number of entries in dialogmap. An alternative that you might want to try implementing is to explicitly pass a name for each thread to the ThrdCtrlPanel constructor. For the purposes of this chapter, simply numbering each thread is sufficient.

Next, the control panel’s position on the screen is offset a bit by calling MoveWindow( ), another Windows API function. This enables multiple panels to be displayed without each one fully covering the one before it. The thread’s priority setting is then displayed in the priority list box by calling the Windows API function SendDlgItemMessage( ).

Finally, the current thread has its priority increased to above normal. This ensures that the application receives enough CPU time to be responsive to user input no matter what is the priority level of the thread under control. This step may not be needed in all cases. You can experiment to find out.

The ThreadPanel( ) Function

ThreadPanel( ) is the Windows callback function that responds to user interaction with the thread control panel. Like all dialog box callback functions, it receives a message each time the user changes the state of a control. It is passed the handle of the dialog box in which the action occurred, the message, and any additional information required by the message. Its general mode of operation is the same as that for any other callback function used by a dialog box. The following discussion describes what happens for each message.

When the thread control panel dialog box is first created, it receives a WM_INITDIALOG message, which is handled by this case sequence:

caseWM_INITDIALOG:
  // Initialize priority list box.
  for(i=0; i<NUMPRIORITIES i++) {
   SendDlgItemMessage(hwnd, IDD_LB,
       LB_ADDSTRING, 0, (LPARAM) priorities[i]);
   }
   // Set Suspend and Resume buttons for thread.
   hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
   hpbRes = GetDlgItem(hwnd, IDD_RESUME);
   EnableWindow(hpbSus, true);  // enable Suspend
   EnableWindow(hpbRes, false); // disable Resume
   return 1;

This initializes the priority list box and sets the Suspend and Resume buttons to their initial states, which are Suspend enabled and Resume disabled.

Each user interaction generates a WM_COMMAND message. Each time this message is received, an iterator to this dialog box’s entry in dialogmap is retrieved, as shown here:

case WM_COMMAND:
  map<HWND, ThreadInfo>::iterator p = dialogmap.find(hwnd);

The information pointed to by p will be used to properly process each action. Because p is an iterator for a map, it points to an object of type pair, which is a structure defined by the STL. This structure contains two fields: first and second. These fields correspond to the information that comprises the key and the value, respectively. In this case, the handle is the key and the thread information is the value.

A code indicating precisely what action has occurred is contained in the low-order word of wParam, which is used to control a switch statement that handles the remaining messages. Each is described next.

When the user presses the Terminate button, the thread under control is stopped. This is handled by this case sequence:

case IDD_TERMINATE:
  TerminateThread(p->second.hThread, 0);
  // Disable Terminate button.
  hpbTerm = GetDlgItem(hwnd, IDD_TERMINATE);
  EnableWindow(hpbTerm, false); // disable
  // Disable Suspend and Resume buttons.
  hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
  hpbRes = GetDlgItem(hwnd, IDD_RESUME);
  EnableWindow(hpbSus, false); // disable Suspend
  EnableWindow(hpbRes, false); // disable Resume
  return 1;

The thread is stopped with a call to TerminateThread( ). Notice how the handle for the thread is obtained. As explained, because p is an iterator for a map, it points to an object of type pair that contains the key in its first field and the value in its second field. This is why the thread handle is obtained by the expression p->second.hThread. After the thread is stopped, the Terminate button is disabled.

Once a thread has been terminated, it cannot be resumed. Notice that the control panel uses TerminateThread( ) to halt execution of a thread. As mentioned earlier, this function must be used with care. If you use the control panel to experiment with threads of your own, you will want to make sure that no harmful side effects are possible.

When the user presses the Suspend button, the thread is suspended. This is accomplished by the following sequence:

case IDD_SUSPEND:
  SuspendThread(p->second.hThread);
  // Set state of the Suspend and Resume buttons.
  hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
  hpbRes = GetDlgItem(hwnd, IDD_RESUME);
  EnableWindow(hpbSus, false); // disable Suspend
  EnableWindow(hpbRes, true);  // enable Resume
  p->second.suspended = true;
  return 1;

The thread is suspended by a call to SuspendThread( ). Next, the state of the Suspend and Resume buttons are updated such that Resume is enabled and Suspend is disabled. This prevents the user from attempting to suspend a thread twice.

A suspended thread is resumed when the Resume button is pressed. It is handled by this code:

case IDD_RESUME:
  ResumeThread(p->second.hThread);
  // Set state of the Suspend and Resume buttons.
  hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
  hpbRes = GetDlgItem(hwnd, IDD_RESUME);
  EnableWindow(hpbSus, true);  // enable Suspend
  EnableWindow(hpbRes, false); // disable Resume
  p->second.suspended = false;
  return 1;

The thread is resumed by a call to ResumeThread( ), and the Suspend and Resume buttons are set appropriately.

To change a thread’s priority, the user double-clicks an entry in the Priority list box. This event is handled as shown next:

case IDD_LB:
  // If a list box entry was double-clicked,
  // then change the priority.
  if(HIWORD(wParam)==LBN_DBLCLK) {
   p->second.priority = SendDlgItemMessage(hwnd,
                        IDD_LB, LB_GETCURSEL,
                        0, 0);
   SetThreadPriority(p->second.hThread,
                     p->second.priority-OFFSET);
  }
  return 1;

List boxes generate various types of notification messages that describe the precise type of event that occurred. Notification messages are contained in the high-order word of wParam. One of these messages is LBN_DBLCLK, which means that the user double-clicked an entry in the box. When this notification is received, the index of the entry is retrieved by calling the Windows API function SendDlgItemMessage( ), requesting the current selection. This value is then used to set the thread’s priority. Notice that OFFSET is subtracted to normalize the value of the index.

Finally, when the user closes the thread control panel dialog box, the IDCANCEL message is sent. It is handled by the following sequence:

case IDCANCEL:
  // If thread is suspended when panel is closed,
  // then resume thread to prevent deadlock.
  if(p->second.suspended) {
   ResumeThread(p->second.hThread);
   p->second.suspended = false;
  }
  // Remove this thread from the list.
  dialogmap.erase(hwnd);
  // Close the panel.
  DestroyWindow(hwnd);
  return 1;

If the thread was suspended, it is restarted. This is necessary to avoid accidentally deadlocking the thread. Next, this dialog box’s entry in dialogmap is removed. Finally, the dialog box is removed by calling the Windows API function DestroyWindow( ).

Here is a program that includes the thread control panel and demonstrates its use. Sample output is shown in Figure 3-2. The program creates a main window and defines two child threads. When started, these threads simply count from 0 to 50,000, displaying the count in the main window. These threads can be controlled by activating a thread control panel.

To use the program, first begin execution of the threads by selecting Start Threads from the Threads menu (or by pressing F2) and then activate the thread control panels by selecting Control Panels from the Threads menu (or by pressing F3). Once the control panels are active, you can experiment with different priority settings and so on.


Figure 3-2.  Sample output from the thread control panel sample program

NOTE


It is beyond the scope of this book to teach Windows programming. However, the operation of this sample program is straightforward and should be easily understood by all Windows programmers.

// Demonstrate the thread control panel.
#include <windows.h>
#include <process.h>
#include "thrdapp.h"
#include "tcp.cpp"
const int MAX = 500000;
LRESULT CALLBACK WindowFunc(HWND, UINT, WPARAM, LPARAM);
unsigned __stdcall MyThread1(void * param);
unsigned __stdcall MyThread2(void * param);
char str[255]; // holds output strings
unsigned tid1, tid2; // thread IDs
HANDLE hThread1, hThread2; // thread handles
HINSTANCE hInst; // instance handle
int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hPrevInst,
                  LPSTR args, int winMode)
{
  HWND hwnd;
  MSG msg;
  WNDCLASSEX wcl;
  HACCEL hAccel;
  // Define a window class.
  wcl.cbSize = sizeof(WNDCLASSEX);
  wcl.hInstance = hThisInst;     // handle to this instance
  wcl.lpszClassName = "MyWin";   // window class name
  wcl.lpfnWndProc = WindowFunc;  // window function
  wcl.style = 0;                 // default style
  wcl.hIcon = LoadIcon(NULL, IDI_APPLICATION); // large icon
  wcl.hIconSm = NULL; // use small version of large icon
  wcl.hCursor = LoadCursor(NULL, IDC_ARROW); // cursor style
  wcl.lpszMenuName = "ThreadAppMenu"; // main menu
  wcl.cbClsExtra = 0; // no extra memory needed
  wcl.cbWndExtra = 0;
  // Make the window background white.
  wcl.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
  // Register the window class.
  if(!RegisterClassEx(&wcl)) return 0;
  /* Now that a window class has been registered, a window
    can be created. */
  hwnd = CreateWindow(
   wcl.lpszClassName, // name of window class
   "Using a Thread Control Panel", // title
   WS_OVERLAPPEDWINDOW, // window style - normal
   CW_USEDEFAULT, // X coordinate - let Windows decide
   CW_USEDEFAULT, // Y coordinate - let Windows decide
   260,           // width
   200,           // height
   NULL,          // no parent window
   NULL,          // no override of class menu
   hThisInst,     // instance handle
   NULL           // no additional arguments
  );
  hInst = hThisInst; // save instance handle
  // Load the keyboard accelerators.
  hAccel = LoadAccelerators(hThisInst, "ThreadAppMenu");
  // Display the window.
  ShowWindow(hwnd, winMode);
  UpdateWindow(hwnd);
  // Create the message loop.
  while(GetMessage(&msg, NULL, 0, 0))
  {
   if(!TranslateAccelerator(hwnd, hAccel, &msg)) {  
     TranslateMessage(&msg); // translate keyboard messages
     DispatchMessage(&msg); // return control to Windows
   }
  }
  return msg.wParam;
}
/* This function is called by Windows and is passed
  messages from the message queue.
*/
LRESULT CALLBACK WindowFunc(HWND hwnd, UINT message,
                           WPARAM wParam, LPARAM lParam)
{
  int response;
  switch(message) {
   case WM_COMMAND:
     switch(LOWORD(wParam)) {
       case IDM_THREAD: // create the threads
         hThread1 = (HANDLE) _beginthreadex(NULL, 0,
                              MyThread1, (void *) hwnd,
                              0, &tid1);
         hThread2 = (HANDLE) _beginthreadex(NULL, 0,
                              MyThread2, (void *) hwnd,
                              0, &tid2);
         break;
       case IDM_PANEL: // activate control panel
         ThrdCtrlPanel(hInst, hThread1);
         ThrdCtrlPanel(hInst, hThread2);
         break;
       case IDM_EXIT:
         response = MessageBox(hwnd, "Quit the Program?",
                               "Exit", MB_YESNO);
         if(response == IDYES) PostQuitMessage(0);
         break;
       case IDM_HELP:
         MessageBox(hwnd,
                    "F1: Help\nF2: Start Threads\nF3: Panel",
                    "Help", MB_OK);
         break;
     }
     break;
   case WM_DESTROY: // terminate the program
     PostQuitMessage(0);
     break;
   default:
    return DefWindowProc(hwnd, message, wParam, lParam);
  }
  return 0;
}
// First thread.
unsigned __stdcall MyThread1(void * param)
{
  int i;
  HDC hdc;
  for(i=0; i<MAX; i++) {
   wsprintf(str, "Thread 1: loop # %5d ", i);
   hdc = GetDC((HWND) param);
   TextOut(hdc, 1, 1, str, lstrlen(str));
   ReleaseDC((HWND) param, hdc);
  }
  return 0;
}
// Second thread.
unsigned __stdcall MyThread2(void * param)
{
  int i;
  HDC hdc;
  for(i=0; i<MAX; i++) {
   wsprintf(str, "Thread 2: loop # %5d ", i);
   hdc = GetDC((HWND) param);
   TextOut(hdc, 1, 20, str, lstrlen(str));
   ReleaseDC((HWND) param, hdc);
  }
  return 0;
}

This program requires the header file thrdapp.h, shown here:

#define IDM_THREAD      100
#define IDM_HELP        101
#define IDM_PANEL       102
#define IDM_EXIT        103

The resource file required by the program is shown here:

#include <windows.h>
#include "thrdapp.h"
#include "tcp.rc"
ThreadAppMenu MENU
{
  POPUP "&Threads" {
   MENUITEM "&Start Threads\tF2", IDM_THREAD
   MENUITEM "&Control Panels\tF3", IDM_PANEL
   MENUITEM "E&xit\tCtrl+X", IDM_EXIT
  }
  MENUITEM "&Help", IDM_HELP
}
ThreadAppMenu ACCELERATORS
{
  VK_F1, IDM_HELP, VIRTKEY
  VK_F2, IDM_THREAD, VIRTKEY
  VK_F3, IDM_PANEL, VIRTKEY
  "^X", IDM_EXIT
}

Although controlling threads using the thread control panel is useful when developing multithreaded programs, ultimately it is using threads that makes them important. Toward this end, this chapter shows a multithreaded version of the GCPtr garbage collector class originally developed in Chapter 2. Recall that the version of GCPtr shown in Chapter 2 collected unused memory each time a GCPtr object went out of scope. Although this approach is fine for some applications, often a better alternative is have the garbage collector run as a background task, recycling memory whenever free CPU cycles are available. The implementation developed here is designed for Windows, but the same basic techniques apply to other multithreaded environments.

To convert GCPtr into a background task is actually fairly easy, but it does involve a number of changes. Here are the main ones:

  1. Member variables that support the thread must be added to GCPtr. These variables include the thread handle, the mutex handle, and an instance counter that keeps track of the number of GCPtr objects in existence.
  2. The constructor for GCPtr must begin the garbage collection thread. The constructor must also create the mutex that controls synchronization. This must happen only once, when the first GCPtr object is created.
  3. Another exception must be defined that will be used to indicate a time-out condition.
  4. The GCPtr destructor must no longer call collect( ). Garbage collection is handled by the garbage collection thread.
  5. A function called gc( ) that serves as the thread entry point for the garbage collector must be defined.
  6. A function called isRunning( ) must be defined. It returns true if the garbage collection is in use.
  7. The member functions of GCPtr that access the garbage collection list contained in gclist must be synchronized so that only one thread at a time can access the list.

The following sections show the changes.

The Additional Member Variables

The multithreaded version of GCPtr requires that the following member variables be added:

// These support multithreading.
unsigned tid; // thread id
static HANDLE hThrd;    // thread handle
static HANDLE hMutex;   // handle of mutex
static int instCount;   // counter of GCPtr objects

The ID of the thread used by the garbage collector is stored in tid. This member is unused except in the call to _beginthreadex( ). The handle to the thread is stored in hThrd. The handle of the mutex used to synchronize access to GCPtr is stored in hMutex. A count of GCPtr objects in existence is maintained in instCount. The last three are static because they are shared by all instances of GCPtr. They are defined like this, outside of GCPtr:

template <class T, int size>
  int GCPtr<T, size>::instCount = 0;
template <class T, int size>
  HANDLE GCPtr<T, size>::hMutex = 0;
template <class T, int size>
  HANDLE GCPtr<T, size>::hThrd = 0;

The Multithreaded GCPtr Constructor

In addition to its original duties, the multithreaded GCPtr( ) must create the mutex, start the garbage collector thread, and update the instance counter. Here is the updated version:

// Construct both initialized and uninitialized objects. GCPtr(T *t=NULL) {
  // When first object is created, create the mutex
  // and register shutdown().
  if(hMutex==0) {
   hMutex = CreateMutex(NULL, 0, NULL);
   atexit(shutdown);
  }
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
   throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  p = findPtrInfo(t);
  // If t is already in gclist, then
  // increment its reference count.
  // Otherwise, add it to the list.
  if(p != gclist.end())
   p->refcount++; // increment ref count
  else {
   // Create and store this entry.
   GCInfo<T> gcObj(t, size);
   gclist.push_front(gcObj);
  }
  addr = t;
  arraySize = size;
  if(size > 0) isArray = true;
  else isArray = false;
  // Increment instance counter for each new object.
  instCount++;
  // If the garbage collection thread is not
  // currently running, start it running.
  if(hThrd==0) {
   hThrd = (HANDLE) _beginthreadex(NULL, 0, gc,
           (void *) 0, 0, (unsigned *) &tid);
   // For some applications, it will be better
   // to lower the priority of the garbage collector
   // as shown here:
   //
   // SetThreadPriority(hThrd,
   //                   THREAD_PRIORITY_BELOW_NORMAL);
  }
  ReleaseMutex(hMutex);
}

Let’s examine this code closely. First, if hMutex is zero, it means that this is the first GCPtr object to be created and no mutex has yet been created for the garbage collector. If this is the case, the mutex is created and its handle is assigned to hMutex. At the same time, the function shutdown( ) is registered as a termination function by calling atexit( ).

It is important to note that in the multithreaded garbage collector, shutdown( ) serves two purposes. First, as in the original version of GCPtr, shutdown( ) frees any unused memory that has not been released because of a circular reference. Second, when a program using the multithreaded garbage collector ends, it stops the garbage collection thread. This means that there might still be dynamically allocated objects that haven’t been freed. This is important because these objects might have destructors that need to be called. Because shutdown( ) releases all remaining objects, it also releases these objects.

Next, the mutex is acquired by calling WaitForSingleObject( ). This is necessary to prevent two threads from accessing gclist at the same time. Once the mutex has been acquired, a search of gclist is made, looking for any preexisting entry that matches the address in t. If one is found, its reference count is incremented. If no preexising entry matches t, a new GCInfo object is created that contains this address, and this object is added to gclist. Then, addr, arraySize, and isArray are set. These actions are the same as in the original version of GCPtr.

Next, instCount is incremented. Recall that instCount is initialized to zero. Incrementing it each time an object is created keeps track of how many GCPtr objects are in existence. As long as this count is above zero, the garbage collector will continue to execute.

Next, if hThrd is zero (as it is initially), then no thread has yet been created for the garbage collector. In this case, _beginthreadex( ) is called to begin the thread. A handle to the thread is then assigned to hThrd. The thread entry function is called gc( ), and it is examined shortly.

Finally, the mutex is released and the constructor returns. It is important to point out that each call to WaitForSingleObject( ) must be balanced by a call to ReleaseMutex( ), as shown in the GCPtr constructor. Failure to release the mutex will cause deadlock.

The TimeOutExc Exception

As you probably noticed in the code for GCPtr( ) described in the preceding section, if the mutex cannot be acquired after 10 seconds, then a TimeOutExc is thrown. Frankly, 10 seconds is a very long time, so a time-out shouldn’t ever happen unless something disrupts the task scheduler of the operating system. However, in the event it does occur, your application code may want to catch this exception. The TimeOutExc class is shown here:

// Exception thrown when a time-out occurs
// when waiting for access to hMutex.
//
class TimeOutExc {
  // Add functionality if needed by your application.
};

Notice that it contains no members. Its existence as a unique type is sufficient for the purposes of this chapter. Of course, you can add functionality if desired.

The Multithreaded GCPtr Destructor

Unlike the single-threaded version of the GCPtr destructor, the multithreaded version of ~GCPtr( ) does not call collect( ). Instead, it simply decrements the reference count of the memory pointed to by the GCPtr that is going out of scope. The actual collection of garbage (if any exists) is handled by the garbage collection thread. The destructor also decrements the instance counter, instCount.

The multithreaded version of ~GCPtr( ) is shown here:

// Destructor for GCPtr.
template <class T, int size>
GCPtr<T, size>::~GCPtr() {
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
   throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  p = findPtrInfo(addr);
  if(p->refcount) p->refcount--; // decrement ref count
  // Decrement instance counter for each object
  // that is destroyed.
  instCount--;
  ReleaseMutex(hMutex);
}

The gc( ) Function

The entry function for the garbage collector is called gc( ), and it is shown here:

// Entry point for garbage collector thread.
template <class T, int size>
unsigned __stdcall GCPtr<T, size>::gc(void * param) {
  #ifdef DISPLAY
   cout << "Garbage collection started.\n";
  #endif
  while(isRunning()) {
     collect();
  }
  collect(); // collect garbage on way out
  // Release and reset the thread handle so
  // that the garbage collection thread can
  // be restarted if necessary.
  CloseHandle(hThrd);
  hThrd = 0;
  #ifdef DISPLAY
   cout << "Garbage collection terminated for "
        << typeid(T).name() << "\n";
  #endif
  return 0;
}

The gc( ) function is quite simple: it runs as long as the garbage collector is in use. The isRunning( ) function returns true if instCount is greater than zero (which means that the garbage collector is still needed) and false otherwise. Inside the loop, collect( ) is called continuously. This approach is suitable for demonstrating the multithreaded garbage collector, but it is probably too inefficient for real-world use. You might want to experiment with calling collect( ) less often, such as only when memory runs low. You could also experiment by calling the Windows API function Sleep( ) after each call to collect( ). Sleep( ) pauses the execution of the calling thread for a specified number of milliseconds. While sleeping, a thread does not consume CPU time.

When isRunning( ) returns false, the loop ends, causing gc( ) to eventually end, which stops the garbage collection thread. Because of the multithreading, it is possible that there will still be an entry on gclist that has not yet been freed even though isRunning( ) returns false. To handle this case, a final call to collect( ) is made before gc( ) ends.

Finally, the thread handle is released via a call to the Windows API function CloseHandle( ), and its value is set to zero. Setting hThrd to zero enables the GCPtr constructor to restart the thread if later in the program new GCPtr objects are created.

The isRunning( ) Function

The isRunning( ) function is shown here:

// Returns true if the collector is still in use.
static bool isRunning() { return instCount > 0; }

It simply compares instCount to zero. As long as instCount is greater than 0, at least one GCPtr pointer is still in existence and the garbage collector is still needed.

Many of the functions in GCPtr access gclist, which holds the garbage collection list. Access to gclist must be synchronized to prevent two or more threads from attempting to use it at the same time. The reason for this is easy to understand. If access were not synchronized, then, for example, one thread might be obtaining an iterator to the end of the list at the same time that another thread is adding or deleting an element from the list. In this case, the iterator would be invalid. To prevent such problems, each sequence of code that accesses gclist must be guarded by a mutex. The copy constructor for GCPtr shown here is one example:

// Copy constructor.
GCPtr(const GCPtr &ob) {
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
   throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  p = findPtrInfo(ob.addr);
  p->refcount++; // increment ref count
  addr = ob.addr;
  arraySize = ob.arraySize;
  if(arraySize > 0) isArray = true;
  else isArray = false;
  instCount++; // increase instance count for copy
  ReleaseMutex(hMutex);
}

Notice that the first thing that the copy constructor does is acquire the mutex. Once acquired, it creates a copy of the object and adjusts the reference count for the memory being pointed to. On its way out, the copy constructor releases the mutex. This same basic method is applied to all functions that access gclist.

Two Other Changes

There are two other changes that you must make to the original version of the garbage collector. First, recall that the original version of GCPtr defined a static variable called first that indicated when the first GCPtr was created. This variable is no longer needed because hMutex now performs this function. Thus, remove first from GCPtr. Because it is a static variable, you will also need to remove its definition outside of GCPtr.

In the original, single-threaded version of the garbage collector, if you defined the DISPLAY macro, you could watch the garbage collector in action. Most of that code has been removed in the multithreaded version because multithreading causes the output to be scrambled and unintelligible in most cases. For the multithreaded version, defining DISPLAY simply lets you know when the garbage collector has started and when it has stopped.

The entire multithreaded version of the garbage collector is shown here. Call this file gcthrd.h.

// A garbage collector that runs as a back ground task.
#include <iostream>
#include <list>
#include <typeinfo>
#include <cstdlib>
#include <windows.h>
#include <process.h>
using namespace std;
// To watch the action of the garbage collector, define DISPLAY.
// #define DISPLAY
// Exception thrown when an attempt is made to
// use an Iter that exceeds the range of the
// underlying object.
//
class OutOfRangeExc {
  // Add functionality if needed by your application.
};
// Exception thrown when a time-out occurs
// when waiting for access to hMutex.
//
class TimeOutExc {
  // Add functionality if needed by your application.
};
// An iterator-like class for cycling through arrays
// that are pointed to by GCPtrs. Iter pointers
// ** do not ** participate in or affect garbage
// collection. Thus, an Iter pointing to
// some object does not prevent that object
// from being recycled.
//
template <class T> class Iter {
  T *ptr;   // current pointer value
  T *end;   // points to element one past end
  T *begin; // points to start of allocated array
  unsigned length; // length of sequence
public:
  Iter() {
   ptr = end = begin = NULL;
   length = 0;
  }
  Iter(T *p, T *first, T *last) {
   ptr = p;
   end = last;
   begin = first;
   length = last - first;
  }
  // Return length of sequence to which this
  // Iter points.
  unsigned size() { return length; }
  // Return value pointed to by ptr.
  // Do not allow out-of-bounds access.
  T &operator*() {
   if( (ptr >= end) || (ptr < begin) )
     throw OutOfRangeExc();
   return *ptr;
  }
  // Return address contained in ptr.
  // Do not allow out-of-bounds access.
  T *operator->() {
   if( (ptr >= end) || (ptr < begin) )
     throw OutOfRangeExc();
   return ptr;
  }
  // Prefix ++.
  Iter operator++() {
   ptr++;
   return *this;
  }
  // Prefix --.
  Iter operator--() {
   ptr--;
   return *this;
  }
  // Postfix ++.
  Iter operator++(int notused) {
   T *tmp = ptr;
   ptr++;
   return Iter<T>(tmp, begin, end);
  }
  // Postfix --.
  Iter operator--(int notused) {
   T *tmp = ptr;
   ptr--;
   return Iter<T>(tmp, begin, end);
  }
  // Return a reference to the object at the
  // specified index. Do not allow out-of-bounds
  // access.
  T &operator[](int i) {
   if( (i < 0) || (i >= (end-begin)) )
     throw OutOfRangeExc();
   return ptr[i];
  }
  // Define the relational operators.
  bool operator==(Iter op2) {
   return ptr == op2.ptr;
  }
  bool operator!=(Iter op2) {
   return ptr != op2.ptr;
  }
  bool operator<(Iter op2) {
   return ptr < op2.ptr;
  }
  bool operator<=(Iter op2) {
   return ptr <= op2.ptr;
  }
  bool operator>(Iter op2) {
   return ptr > op2.ptr;
  }
  bool operator>=(Iter op2) {
   return ptr >= op2.ptr;
  }
  // Subtract an integer from an Iter.
  Iter operator-(int n) {
   ptr -= n;
   return *this;
  }
  // Add an integer to an Iter.
  Iter operator+(int n) {
   ptr += n;
   return *this;
  }
  // Return number of elements between two Iters.
  int operator-(Iter<T> &itr2) {
   return ptr - itr2.ptr;
 }
};
// This class defines an element that is stored
// in the garbage collection information list.
//
template <class T> class GCInfo {
public:
  unsigned refcount; // current reference count
  T *memPtr; // pointer to allocated memory
  /* isArray is true if memPtr points
    to an allocated array. It is false
    otherwise. */
  bool isArray; // true if pointing to array
  /* If memPtr is pointing to an allocated
    array, then arraySize contains its size */
  unsigned arraySize; // size of array
  // Here, mPtr points to the allocated memory.
  // If this is an array, then size specifies
  // the size of the array.
  GCInfo(T *mPtr, unsigned size=0) {
   refcount = 1;
   memPtr = mPtr;
   if(size != 0)
     isArray = true;
   else
     isArray = false;
   arraySize = size;
  }
};
// Overloading operator== allows GCInfos to be compared.
// This is needed by the STL list class.
template <class T> bool operator==(const GCInfo<T> &ob1,
               const GCInfo<T> &ob2) {
  return (ob1.memPtr == ob2.memPtr);
}
// GCPtr implements a pointer type that uses
// garbage collection to release unused memory.
// A GCPtr must only be used to point to memory
// that was dynamically allocated using new.
// When used to refer to an allocated array,
// specify the array size.
//
template <class T, int size=0> class GCPtr {
  // gclist maintains the garbage collection list.
  static list<GCInfo<T> > gclist;
  // addr points to the allocated memory to which
  // this GCPtr pointer currently points.
  T *addr;
  /* isArray is true if this GCPtr points
    to an allocated array. It is false
    otherwise. */
  bool isArray; // true if pointing to array
  // If this GCPtr is pointing to an allocated
  // array, then arraySize contains its size.
  unsigned arraySize; // size of the array
  // These support multithreading.
  unsigned tid; // thread id
  static HANDLE hThrd; // thread handle
  static HANDLE hMutex; // handle of mutex
  static int instCount; // counter of GCPtr objects
  // Return an iterator to pointer info in gclist.
  typename list<GCInfo<T> >::iterator findPtrInfo(T *ptr);
public:
  // Define an iterator type for GCPtr<T>.
  typedef Iter<T> GCiterator;
  // Construct both initialized and uninitialized objects.
  GCPtr(T *t=NULL) {
   // When first object is created, create the mutex
   // and register shutdown().
   if(hMutex==0) {
     hMutex = CreateMutex(NULL, 0, NULL);
     atexit(shutdown);
   }
   if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
     throw TimeOutExc();
   list<GCInfo<T> >::iterator p;
   p = findPtrInfo(t);
   // If t is already in gclist, then
   // increment its reference count.
   // Otherwise, add it to the list.
   if(p != gclist.end())
     p->refcount++; // increment ref count
   else {
     // Create and store this entry.
     GCInfo<T> gcObj(t, size);
     gclist.push_front(gcObj);
   }
   addr = t;
   arraySize = size;
   if(size > 0) isArray = true;
   else isArray = false;
   // Increment instance counter for each new object.
   instCount++;
   // If the garbage collection thread is not
   // currently running, start it running.
   if(hThrd==0) {
     hThrd = (HANDLE) _beginthreadex(NULL, 0, gc,
             (void *) 0, 0, (unsigned *) &tid);
   // For some applications, it will be better
   // to lower the priority of the garbage collector
   // as shown here:
   //
   // SetThreadPriority(hThrd,
   //                   THREAD_PRIORITY_BELOW_NORMAL);
  }
  ReleaseMutex(hMutex);
}
// Copy constructor.
GCPtr(const GCPtr &ob) {
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
   throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  p = findPtrInfo(ob.addr);
  p->refcount++; // increment ref count
  addr = ob.addr;
  arraySize = ob.arraySize;
  if(arraySize > 0) isArray = true;
  else isArray = false;
  instCount++; // increase instance count for copy
  ReleaseMutex(hMutex);
}
// Destructor for GCPTr.
~GCPtr();
// Collect garbage. Returns true if at least
// one object was freed.
static bool collect();
// Overload assignment of pointer to GCPtr.
T *operator=(T *t);
// Overload assignment of GCPtr to GCPtr.
GCPtr &operator=(GCPtr &rv);
// Return a reference to the object pointed
// to by this GCPtr.
T &operator*() {
  return *addr;
}
// Return the address being pointed to.
T *operator->() { return addr; }
// Return a reference to the object at the
// index specified by i.
T &operator[](int i) {
  return addr[i];
}
// Conversion function to T *.
operator T *() { return addr; }
// Return an Iter to the start of the allocated memory. Iter<T> begin() {
  int size;
  if(isArray) size = arraySize;
  else size = 1;
  return Iter<T>(addr, addr, addr + size);
}
// Return an Iter to one past the end of an allocated array.
Iter<T> end() {
  int size;
  if(isArray) size = arraySize;
  else size = 1;
  return Iter<T>(addr + size, addr, addr + size);
}
// Return the size of gclist for this type
// of GCPtr.
  static int gclistSize() {
   if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
     throw TimeOutExc();
   unsigned sz = gclist.size();
   ReleaseMutex(hMutex);
   return sz;
  }
  // A utility function that displays gclist.
  static void showlist();
  // The following functions support multithreading.
  //
  // Returns true if the collector is still in use.
  static bool isRunning() { return instCount > 0; }
  // Clear gclist when program exits.
  static void shutdown();
  // Entry point for garbage collector thread.
  static unsigned __stdcall gc(void * param);
};
// Create storage for the static variables.
template <class T, int size>
  list<GCInfo<T> > GCPtr<T, size>::gclist;
template <class T, int size>
  int GCPtr<T, size>::instCount = 0;
template <class T, int size>
  HANDLE GCPtr<T, size>::hMutex = 0;
template <class T, int size>
  HANDLE GCPtr<T, size>::hThrd = 0;
// Destructor for GCPtr.
template <class T, int size>
GCPtr<T, size>::~GCPtr() {
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
   throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  p = findPtrInfo(addr);
  if(p->refcount) p->refcount--; // decrement ref count
  // Decrement instance counter for each object
  // that is destroyed.
  instCount--;
  ReleaseMutex(hMutex);
}
// Collect garbage. Returns true if at least
// one object was freed.
template <class T, int size>
bool GCPtr<T, size>::collect() {
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
   throw TimeOutExc();
  bool memfreed = false;
  list<GCInfo<T> >::iterator p;
  do {
   // Scan gclist looking for unreferenced pointers.
   for(p = gclist.begin(); p != gclist.end(); p++) {
     // If in-use, skip.
     if(p->refcount > 0) continue;
     memfreed = true;
     // Remove unused entry from gclist.
     gclist.remove(*p);
     // Free memory unless the GCPtr is null.
     if(p->memPtr) {
       if(p->isArray) {
         delete[] p->memPtr; // delete array
       }
       else {
         delete p->memPtr; // delete single element
       }
     }
     // Restart the search.
     break;
   }
  } while(p != gclist.end());
  ReleaseMutex(hMutex);
  return memfreed;
}
// Overload assignment of pointer to GCPtr.
template <class T, int size>
T * GCPtr<T, size>::operator=(T *t) {
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
   throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  // First, decrement the reference count
  // for the memory currently being pointed to.
  p = findPtrInfo(addr);
  p->refcount--;
  // Next, if the new address is already
  // existent in the system, increment its
  // count. Otherwise, create a new entry
  // for gclist.
  p = findPtrInfo(t);
  if(p != gclist.end())
   p->refcount++;
  else {
   // Create and store this entry.
   GCInfo<T> gcObj(t, size);
   gclist.push_front(gcObj);
  }
  addr = t; // store the address.
  ReleaseMutex(hMutex);
  return t;
}
// Overload assignment of GCPtr to GCPtr.
template <class T, int size>
GCPtr<T, size> & GCPtr<T, size>::operator=(GCPtr &rv) {
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
   throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  // First, decrement the reference count
  // for the memory currently being pointed to.
  p = findPtrInfo(addr);
  p->refcount--;
  // Next, increment the reference count of
  // of the new object.
  p = findPtrInfo(rv.addr);
  p->refcount++; // increment ref count
  addr = rv.addr;// store the address.
  ReleaseMutex(hMutex);
  return rv;
}
// A utility function that displays gclist.
template <class T, int size>
void GCPtr<T, size>::showlist() {
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
   throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  cout << "gclist<" << typeid(T).name() << ", "
      << size << ">:\n";
  cout << "memPtr     refcount    value\n";
  if(gclist.begin() == gclist.end()) {
   cout << "           -- Empty --\n\n";
   return;
  }
  for(p = gclist.begin(); p != gclist.end(); p++) {
   cout <<  "[" << (void *)p->memPtr << "]"
        << "     " << p->refcount << "      ";
   if(p->memPtr) cout << "  " << *p->memPtr;
   else cout << "   ---";
   cout << endl;
  }
  cout << endl;
  ReleaseMutex(hMutex);
}
// Find a pointer in gclist.
template <class T, int size>
typename list<GCInfo<T> >::iterator
  GCPtr<T, size>::findPtrInfo(T *ptr) {
  list<GCInfo<T> >::iterator p;
  // Find ptr in gclist.
  for(p = gclist.begin(); p != gclist.end(); p++)
   if(p->memPtr == ptr)
     return p;
  return p;
}
// Entry point for garbage collector thread.
template <class T, int size>
unsigned __stdcall GCPtr<T, size>::gc(void * param) {
  #ifdef DISPLAY
   cout << "Garbage collection started.\n";
  #endif
  while(isRunning()) {
     collect();
  }
  collect(); // collect garbage on way out
  // Release and reset the thread handle so
  // that the garbage collection thread can
  // be restarted if necessary.
  CloseHandle(hThrd);
  hThrd = 0;
  #ifdef DISPLAY
   cout << "Garbage collection terminated for "
        << typeid(T).name() << "\n";
  #endif
  return 0;
}
// Clear gclist when program exits.
template <class T, int size>
void GCPtr<T, size>::shutdown() {
  if(gclistSize() == 0) return; // list is empty
  list<GCInfo<T> >::iterator p;
  #ifdef DISPLAY
   cout << "Before collecting for shutdown() for "
        << typeid(T).name() << "\n";
  #endif
  for(p = gclist.begin(); p != gclist.end(); p++) {
   // Set all remaining reference counts to zero.
   p->refcount = 0;
  }
  collect();
  #ifdef DISPLAY
   cout << "After collecting for shutdown() for "
        << typeid(T).name() << "\n";
  #endif
}

To use the multithreaded garbage collector, include gcthrd.h in your program. Then, use GCPtr in the same way as described in Chapter 2. When you compile the program, you must remember to link in the multithreaded libraries, as explained earlier in this chapter in the section describing _beginthreadex( ) and endthreadex( ).

To see the effects of the multithreaded garbage collector, try this version of the load test program originally shown in Chapter 2:

// Demonstrate the multithreaded garbage collector. #include <iostream>
#include <new>
#include "gcthrd.h"
using namespace std;
// A simple class for load testing GCPtr.
class LoadTest {
  int a, b;
public:
  double n[100000]; // just to take-up memory
  double val;
  LoadTest() { a = b = 0; }
  LoadTest(int x, int y) {
   a = x;
   b = y;
   val = 0.0;
  }

  friend ostream &operator<(ostream &strm, LoadTest &obj);
};
// Create an insertor for LoadTest.
ostream &operator<(ostream &strm, LoadTest &obj) {
  strm << "(" << obj.a << " " << obj.b << ")";
  return strm;
}
int main() {
  GCPtr<LoadTest> mp;
  int i;
  for(i = 1; i < 2000; i++) {
   try {
     mp = new LoadTest(i, i);
     if(!(i%100))
       cout << "gclist contains " << mp.gclistSize()
            << " entries.\n";
   } catch(bad_alloc xa) {
     // For most users, this exception won't
     // ever occur.
     cout << "Last object: " << *mp << endl;
     cout << "Length of gclist: "
          << mp.gclistSize() << endl;
   }
  }
  return 0;
}

Here is a sample run. (Of course, your output may vary.) This output was produced with the display option turned on by defining DISPLAY within gcthrd.h.

Garbage collection started.
gclist contains 42 entries.
gclist contains 35 entries.
gclist contains 29 entries.
gclist contains 22 entries.
gclist contains 18 entries.
gclist contains 11 entries.
gclist contains 4 entries.
gclist contains 51 entries.
gclist contains 47 entries.
gclist contains 40 entries.
gclist contains 33 entries.
gclist contains 26 entries.
gclist contains 19 entries.
gclist contains 15 entries.
gclist contains 10 entries.
gclist contains 3 entries.
gclist contains 53 entries.
gclist contains 46 entries.
gclist contains 42 entries.
Before collecting for shutdown() for class LoadTest
After collecting for shutdown() for class LoadTest

As you can see, because collect( ) is running in the background, gclist never gets very large, even though thousands of objects are being allocated and abandoned.

Some Things to Try

Creating successful multithreaded programs can be quite challenging. One reason for this is the fact that multithreading requires that you think of programs in parallel rather than linear terms. Furthermore, at runtime, threads interact in ways that are often difficult to anticipate. Thus, you might be surprised (or even bewildered) by the actions of a multithreaded program. The best way to get good at multithreading is to play with it. Toward this end, here are some ideas that you might want to try.

Try adding another list box to the thread control panel that lets the user adjust the priority class of the thread in addition to its priority value. Try adding various synchronization objects to the control panel that can be turned on or off under user control. This will let you experiment with different synchronization options.

For the multithreaded garbage collector, try collecting garbage less often, such as when gclist reaches a certain size or after free memory drops to a predetermined point. Alternatively, you could use a waitable timer to activate garbage collection on a regular basis. Finally, you might want to experiment with the garbage collector’s priority class and settings to find which level is optimal for your use.


"C++" 카테고리의 다른 글
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2007/07/30
  • C++ Preprocessor: The Code in the Middle (0)2007/07/30
  • Multithreading in C++ (0)2007/07/30
  • A Simple Garbage Collector for C++ (0)2007/07/30
  • Function Pointers (1)2007/07/30
2007/07/30 10:29 2007/07/30 10:29
Posted by webdizen
Tags C++Keyword C++, Multithreading
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3108

Leave your greetings.

[로그인][오픈아이디란?]

Programming/C++2007/07/30 10:18

A Simple Garbage Collector for C++

출처 : http://www.devarticles.com/c/a/cplusplu ··· -plus%2F

The use of dynamically allocated memory must be managed, because it has a tremendous effect on the performance of your programs. The current trend in handling dynamic memory seems to be shifting toward an automated approach. While C++ uses the manual approach for managing dynamic memory, this does not mean that it can't be automated in that language -- thus giving the C++ programmer the best of both worlds. This article explains how to do it. It is excerpted from chapter two of The Art of C++, written by Herbert Schildt (McGraw-Hill/Osborne, 2004; ISBN: 0072255129).

Throughout the history of computing, there has been an ongoing debate concerning the best way to manage the use of dynamically allocated memory. Dynamically allocated memory is memory that is obtained during runtime from the heap, which is a region of free memory that is available for program use. The heap is also commonly referred to as free store or dynamic memory. Dynamic allocation is important because it enables a program to obtain, use, release, and then reuse memory during execution. Because nearly all real-world programs use dynamic allocation in some form, the way it is managed has a profound effect on the architecture and performance of programs.

In general, there are two ways that dynamic memory is handled. The first is the manual approach, in which the programmer must explicitly release unused memory in order to make it available for reuse. The second relies on an automated approach, commonly referred to as garbage collection, in which memory is automatically recycled when it is no longer needed. There are advantages and disadvantages to both approaches, and the favored strategy has shifted between the two over time.

C++ uses the manual approach to managing dynamic memory. Garbage collection is the mechanism employed by Java and C#. Given that Java and C# are newer languages, the current trend in computer language design seems to be toward garbage collection. This does not mean, however, that the C++ programmer is left on the “wrong side of history.” Because of the power built into C++, it is possible—even easy—to create a garbage collector for C++. Thus, the C++ programmer can have the best of both worlds: manual control of dynamic allocation when needed and automatic garbage collection when desired.

This chapter develops a complete garbage collection subsystem for C++. At the outset, it is important to understand that the garbage collector does not replace C++’s built-in approach to dynamic allocation. Rather, it supplements it. Thus, both the manual and garbage collection systems can be used within the same program.

Aside from being a useful (and fascinating) piece of code in itself, a garbage collector was chosen for the first example in this book because it clearly shows the unsurpassed power of C++. Through the use of template classes, operator overloading, and C++’s inherent ability to handle the low-level elements upon which the computer operates, such as memory addresses, it is possible to transparently add a core feature to C++. For most other languages, changing the way that dynamic allocation is handled would require a change to the compiler itself. However, because of the unparalleled power that C++ gives the programmer, this task can be accomplished at the source code level.

The garbage collector also shows how a new type can be defined and fully integrated into the C++ programming environment. Such type extensibility is a key component of C++, and it’s one that is often overlooked. Finally, the garbage collector testifies to C++’s ability to “get close to the machine” because it manipulates and manages pointers. Unlike some other languages which prevent access to the low-level details, C++ lets the programmer get as close to the hardware as necessary.

-----------------------------------------------------------------

Before developing a garbage collector for C++, it is useful to compare garbage collection to the manual method that is built-in to C++. Normally, the use of dynamic memory in C++ requires a two-step process. First, memory is allocated from the heap via new. Second, when that memory is no longer needed, it is released by delete. Thus, each dynamic allocation follows this sequence:

  p = new some_object;
 // ...
  delete p;

In general, each use of new must be balanced by a matching delete. If delete is not used, the memory is not released, even if that memory is no longer needed by your program.

Garbage collection differs from the manual approach in one key way: it automates the release of unused memory. Therefore, with garbage collection, dynamic allocation is a one-step operation. For example, in Java and C#, memory is allocated for use by new, but it is never explicitly freed by your program. Instead, the garbage collector runs periodically, looking for pieces of memory to which no other object points. When no other object points to a piece of dynamic memory, it means that there is no program element using that memory. When it finds a piece of unused memory, it frees it. Thus, in a garbage collection system, there is no delete operator, nor a need for one, either.

At first glance, the inherent simplicity of garbage collection makes it seem like the obvious choice for managing dynamic memory. In fact, one might question why the manual method is used at all, especially by a language as sophisticated as C++. However, in the case of dynamic allocation, first impressions prove deceptive because both approaches involve a set of trade-offs. Which approach is most appropriate is decided by the application. The following sections describe some of the issues involved.

The Pros and Cons of Manual Memory Management

The main benefit of manually managing dynamic memory is efficiency. Because there is no garbage collector, no time is spent keeping track of active objects or periodically looking for unused memory. Instead, when the programmer knows that the allocated object is no longer needed, the programmer explicitly frees it and no additional overhead is incurred. Because it has none of the overhead associated with garbage collection, the manual approach enables more efficient code to be written. This is one reason why it was necessary for C++ to support manual memory management: it enabled the creation of high-performance code.

Another advantage to the manual approach is control. Although requiring the programmer to handle both the allocation and release of memory is a burden, the benefit is that the programmer gains complete control over both halves of the process. You know precisely when memory is being allocated and precisely when it is being released. Furthermore, when you release an object via delete, its destructor is executed at that point rather than at some later time, as can be the case with garbage collection. Thus, with the manual method you can control precisely when an allocated object is destroyed.

Although it is efficient, manual memory management is susceptible to a rather annoying type of error: the memory leak. Because memory must be freed manually, it is possible (even easy) to forget to do so. Failing to release unused memory means that the memory will remain allocated even if it is no longer needed. Memory leaks cannot occur in a garbage collection environment because the garbage collector ensures that unused objects are eventually freed. Memory leaks are a particularly troublesome problem in Windows programming, where the failure to release unused resources slowly degrades performance.

Other problems that can occur with C++’s manual approach include the premature releasing of memory that is still in use, and the accidental freeing of the same memory twice. Both of these errors can lead to serious trouble. Unfortunately, they may not show any immediate symptoms, making them hard to find.

The Pros and Cons of Garbage Collection

There are several different ways to implement garbage collection, each offering different performance characteristics. However, all garbage collection systems share a set of common attributes that can be compared against the manual approach. The main advantages to garbage collection are simplicity and safety. In a garbage collection environment, you explicitly allocate memory via new, but you never explicitly free it. Instead, unused memory is automatically recycled. Thus, it is not possible to forget to release an object or to release an object prematurely. This simplifies programming and prevents an entire class of problems. Furthermore, it is not possible to accidentally free dynamically allocated memory twice. Thus, garbage collection provides an easy-to-use, error-free, reliable solution to the memory management problem.

Unfortunately, the simplicity and safety of garbage collection come at a price. The first cost is the overhead incurred by the garbage collection mechanism. All garbage collection schemes consume some CPU cycles because the reclamation of unused memory is not a cost-free process. This overhead does not occur with the manual approach.

A second cost is loss of control over when an object is destroyed. Unlike the manual approach, in which an object is destroyed (and its destructor called) at a known point in time—when a delete statement is executed on that object—garbage collection does not have such a hard and fast rule. Instead, when garbage collection is used, an object is not destroyed until the collector runs and recycles the object, which may not occur until some arbitrary time in the future. For example, the collector might not run until the amount of free memory drops below a certain point. Furthermore, it is not always possible to know the order in which objects will be destroyed by the garbage collector. In some cases, the inability to know precisely when an object is destroyed can cause trouble because it also means that your program can’t know precisely when the destructor for a dynamically allocated object is called.

For garbage collection systems that run as a background task, this loss of control can escalate into a potentially more serious problem for some types of applications because it introduces what is essentially nondeterministic behavior into a program. A garbage collector that executes in the background reclaims unused memory at times that are, for all practical purposes, unknowable. For example, the collector will usually run only when free CPU time is available. Because this might vary from one program run to the next, from one computer to next, or from one operating system to the next, the precise point in program execution at which the garbage collector executes is effectively nondeterministic. This is not a problem for many programs, but it can cause havoc with real-time applications in which the unexpected allocation of CPU cycles to the garbage collector could cause an event to be missed.

You Can Have It Both Ways

As the preceding discussions explained, both manual management and garbage collection maximize one feature at the expense of another. The manual approach maximizes efficiency and control at the expense of safety and ease of use. Garbage collection maximizes simplicity and safety but pays for it with a loss of runtime performance and control. Thus, garbage collection and manual memory management are essentially opposites, each maximizing the traits that the other sacrifices. This is why neither approach to dynamic memory management can be optimal for all programming situations.

Although opposites, the two approaches are not mutually exclusive. They can coexist. Thus, it is possible for the C++ programmer to have access to both approaches, choosing the proper method for the task at hand. All one needs to do is create a garbage collector for C++, and this is the subject of the rest of this chapter.

Creating a Garbage Collector in C++

Because C++ is a rich and powerful language, there are many different ways to implement a garbage collector. One obvious, but limited, approach is to create a garbage collector base class, which is then inherited by classes that want to use garbage collection. This would enable you to implement garbage collection on a class-by-class basis. This solution is, unfortunately, too narrow to be satisfying.

A better solution is one in which the garbage collector can be used with any type of dynamically allocated object. To provide such a solution, the garbage collector must:

  1. Coexist with the built-in, manual method provided by C++.
  2. Not break any preexisting code. Moreover, it must have no impact whatsoever on existing code.
  3. Work transparently so that allocations that use garbage collection are operated on in the same way as those that don’t.
  4. Allocate memory using new in the same way that C++’s built-in approach does.
  5. Work with all data types, including the built-in types such as int and double.
  6. Be simple to use.

In short, the garbage collection system must be able to dynamically allocate memory using a mechanism and syntax that closely resemble that already used by C++ and not affect existing code. At first thought, this might seem to be a daunting task, but it isn’t.

Understanding the Problem

The key challenge that one faces when creating a garbage collector is how to know when a piece of memory is unused. To understand the problem, consider the following sequence:

  int *p;
  p = new int(99);
  p = new int(100);

Here, two int objects are dynamically allocated. The first contains the value 99 and a pointer to this value is stored in p. Next, an integer containing the value 100 is allocated, and its address is also stored in p, thus overwriting the first address. At this point, the memory for int(99) is not pointed to by p (or any other object) and can be freed. The question is, how does the garbage collector know that neither p nor any other object points to int(99)?

Here is a slight variation on the problem:

  int *p, *q;
 
p = new int(99);
 
q = p; // now, q points to same memory as p
 
p = new int(100);

In this case, q points to the memory that was originally allocated for p. Even though p is then pointed to a different piece of memory, the memory that it originally pointed to can’t be freed because it is still in use by q. The question: how does the garbage collector know this fact? The precise way that these questions are answered is determined by the garbage collection algorithm employed.

-----------------------------------------------------------------

Before implementing a garbage collector for C++, it is necessary to decide what garbage collection algorithm to use. The topic of garbage collection is a large one, having been the focus of serious academic study for many years. Because it presents an intriguing problem for which there is a variety of solutions, a number of different garbage collection algorithms have been designed. It is far beyond the scope of this book to examine each in detail. However, there are three archetypal approaches: reference counting, mark and sweep, and copying. Before choosing an approach, it will be useful to review these three algorithms.

Reference Counting

In reference counting, each dynamically allocated piece of memory has associated with it a reference count. This count is incremented each time a reference to the memory is added and decremented each time a reference to the memory is removed. In C++ terms, this means that each time a pointer is set to point to a piece of allocated memory, the reference count associated with that memory is incremented. When the pointer is set to point elsewhere, the reference count is decremented. When the reference count drops to zero, the memory is unused and can be released.

The main advantage of reference counting is simplicity—it is easy to understand and implement. Furthermore, it places no restrictions on the organization of the heap because the reference count is independent of an object’s physical location. Reference counting adds overhead to each pointer operation, but the collection phase is relatively low cost. The main disadvantage is that circular references prevent memory that is otherwise unused from being released. A circular reference occurs when two objects point to each other, either directly or indirectly. In this situation, neither object’s reference count ever drops to zero. Some solutions to the circular reference problem have been devised, but all add complexity and/or overhead.

Mark and Sweep

Mark and sweep involves two phases. In the first phase, all objects in the heap are set to their unmarked state. Then, all objects directly or indirectly accessible from program variables are marked as “in-use.” In phase two, all of allocated memory is scanned (that is, a sweep of memory is made), and all unmarked elements are released.

There are two main advantages of mark and sweep. First, it easily handles circular references. Second, it adds virtually no runtime overhead prior to collection. It has two main disadvantages. First, a considerable amount of time might be spent collecting garbage because the entire heap must be scanned during collection. Thus, garbage collection may cause unacceptable runtime characteristics for some programs. Second, although mark and sweep is simple conceptually, it can be tricky to implement efficiently.

Copying

The copying algorithm organizes free memory into two spaces. One is active (holding the current heap), and the other is idle. During garbage collection, in-use objects from the active space are identified and copied into the idle space. Then, the roles of the two spaces are reversed, with the idle space becoming active and the active space becoming idle. Copying offers the advantage of compacting the heap in the copy process. It has the disadvantage of allowing only half of free memory to be in use at any one time.

Which Algorithm?

Given that there are advantages and disadvantages to all of the three classical approaches to garbage collection, it might seem hard to choose one over the other. However, given the constraints enumerated earlier, there is a clear choice: reference counting. First and most importantly, reference counting can be easily “layered onto” the existing C++ dynamic allocation system. Second, it can be implemented in a straightforward manner and in a way that does not affect preexisting code. Third, it does not require any specific organization or structuring of the heap, thus the standard allocation system provided by C++ is unaffected.

The one drawback to using reference counting is its difficulty in handling circular references. This isn’t an issue for many programs because intentional circular references are not all that common and can usually be avoided. (Even things that we call circular, such as a circular queue, don’t necessarily involve a circular pointer reference.) Of course, there are cases in which circular references are needed. It is also possible to create a circular reference without knowing you have done so, especially when working with third-party libraries. Therefore, the garbage collector must provide some means to gracefully handle a circular reference, should one exist.

To handle the circular reference problem, the garbage collector developed in this chapter will release any remaining allocated memory when the program exits. This ensures that objects involved in a circular reference will be freed and their destructors called. It is important to understand that normally there will be no allocated objects remaining at program termination. This mechanism is explicitly for those objects that can’t be released because of a circular reference. (You might want to experiment with other means of handling the circular reference problem. It presents an interesting challenge.)

Implementing the Garbage Collector

To implement a reference counting garbage collector, there must be some way to keep track of the number of pointers that point to each piece of dynamically allocated memory. The trouble is that C++ has no built-in mechanism that enables one object to know when another object is pointing to it. Fortunately, there is a solution: you can create a new pointer type that supports garbage collection. This is the approach used by the garbage collector in this chapter.

To support garbage collection, the new pointer type must do three things. First, it must maintain a list of reference counts for active dynamically allocated objects. Second, it must keep track of all pointer operations, increasing an object’s reference count each time a pointer is pointed to that object and decreasing the count each time a pointer is redirected to another object. Third, it must recycle those objects whose reference counts drop to zero. Aside from supporting garbage collection, the new pointer type will look and feel just like a normal pointer. For example, all pointer operations, such as * and –>, are supported.

In addition to being a convenient way to implement a garbage collector, the creation of a garbage collection pointer type satisfies the constraint that the original C++ dynamic allocation system must be unaffected. When garbage collection is desired, garbage collection-enabled pointers are used. When garbage collection is not desired, normal C++ pointers are available. Thus, both types of pointers can be used within the same program.

To Multithread or Not?

Another consideration when designing a garbage collector for C++ is whether it should be single-threaded or multithreaded. That is, should the garbage collector be designed as a background process, running in its own thread and collecting garbage as CPU time permits? Or, should the garbage collector run in the same thread as the process that uses it, collecting garbage when certain program conditions occur? Both approaches have advantages and disadvantages.

The main advantage to creating a multithreaded garbage collector is efficiency. Garbage can be collected when idle CPU cycles are available. The disadvantage is, of course, that C++ does not provide any built-in support for multithreading. This means that any multithreaded approach will depend upon operating system facilities to support the multitasking. This makes the code nonportable.

The main advantage to using a single-threaded garbage collector is portability. It can be used in situations that do not support multithreading or in cases in which the price of multithreading is too high. The main disadvantage is that the rest of the program stops when garbage collection takes place.

In this chapter, the single-threaded approach is used because it works in all C++ environments. Thus, it can be used by all readers of this book. However, for those readers wanting a multithreaded solution, one is given in Chapter 3, which deals with the techniques needed to successfully multithread a C++ program in a Windows environment.

When to Collect Garbage?

One final question that needs to be answered before a garbage collector can be implemented: When is garbage collected? This is less of an issue for a multithreaded garbage collector, which can run continuously as a background task, collecting garbage whenever CPU cycles are available, than it is for a single-threaded garbage collector, such as that developed in this chapter, which must stop the rest of the program to collect garbage.

In the real world, garbage collection usually takes place only when there is sufficient reason to do so, such as the case of memory running low. This makes sense for two reasons. First, with some garbage collection algorithms, such as mark and sweep, there is no way to know that a piece of memory is unused without actually performing the collection. (That is, sometimes there is no way to know that there is garbage to be collected without actually collecting it!) Second, collecting garbage is a time-consuming process which should not be performed needlessly.

However, waiting for memory to run low before initiating garbage collection is not suitable for the purposes of this chapter because it makes it next to impossible to demonstrate the collector. Instead, the garbage collector developed in this chapter will collect garbage more frequently so that its actions can easily be observed. As the collector is coded, garbage is collected whenever a pointer goes out of scope. Of course, this behavior can easily be changed to fit your applications.

One last point: when using reference-count based garbage collection, it is technically possible to recycle unused memory as soon as its reference count drops to zero, rather than using a separate garbage collection phase. However, this approach adds overhead to every pointer operation. The method used by this chapter is to simply decrement the memory’s reference count each time a pointer to that memory is redirected and let the collection process handle the actual recycling of memory at more convenient times. This reduces the runtime overhead associated with pointer operations, which one typically wants to be as fast as possible.

As many readers will know, C++ defines a library class called auto_ptr. Because an auto_ptr automatically frees the memory to which it points when it goes out of scope, you might think that it would be of use when developing a garbage collector, perhaps forming the foundation. This is not the case, however. The auto_ptr class is designed for a concept that the ISO C++ Standard calls “strict ownership,” in which an auto_ptr “owns” the object to which it points. This ownership can be transferred to another auto_ptr, but in all cases some auto_ptr will own the object until it is destroyed. Furthermore, an auto_ptr is assigned an address of an object only when it is initialized. After that, you can’t change the memory to which an auto_ptr points, except by assigning one auto_ptr to another. Because of auto_ptr’s “strict ownership” feature, it is not particularly useful when creating a garbage collector, and it is not used by the garbage collectors in this book.

-----------------------------------------------------------------
A Simple C++ Garbage Collector

The entire garbage collector is shown here. As explained, the garbage collector works by creating a new pointer type that provides built-in support for garbage collection based on reference counting. The garbage collector is single-threaded, which means that it is quite portable and does not rely upon (or make assumptions about) the execution environment. This code should be stored in a file called gc.h.

There are two things to notice as you look through the code. First, most of the member functions are quite short and are defined inside their respective classes in the interest of efficiency. Recall that a function defined within its class is automatically in-lined, which eliminates the overhead of a function call. Only a few functions are long enough to require their definition to be outside their class.

Secondly, notice the comment near the top of the file. If you want to watch the action of the garbage collector, simply turn on the display option by defining the macro called DISPLAY. For normal use, leave DISPLAY undefined.

// A single-threaded garbage collector.
#include <iostream>
#include <list>
#include <typeinfo>
#include <cstdlib>
using namespace std;
// To watch the action of the garbage collector, define DISPLAY.
// #define DISPLAY
// Exception thrown when an attempt is made to
// use an Iter that exceeds the range of the
// underlying object.
//
class OutOfRangeExc {
 
// Add functionality if needed by your application.
};
// An iterator-like class for cycling through arrays
// that are pointed to by GCPtrs. Iter pointers
// ** do not ** participate in or affect garbage
// collection. Thus, an Iter pointing to
// some object does not prevent that object
// from being recycled.
//
template <class T> class Iter {
  T *ptr;   // current pointer value
 
T *end;   // points to element one past end
 
T *begin; // points to start of allocated array
  unsigned length; // length of sequence
public:
 
Iter() {
   ptr = end = begin = NULL;
   length = 0;
 
}
 
Iter(T *p, T *first, T *last) {
   ptr = p;
   end = last;
   begin = first;
   length = last - first;
  }
  // Return length of sequence to which this
  // Iter points.
  unsigned size() { return length; }
 
// Return value pointed to by ptr.
  // Do not allow out-of-bounds access.
  T &operator*() {
  
if( (ptr >= end) || (ptr < begin) )
     throw OutOfRangeExc();
   return *ptr;
  }
 
// Return address contained in ptr.
  // Do not allow out-of-bounds access.
  T *operator->() {
  
if( (ptr >= end) || (ptr < begin) )
     throw OutOfRangeExc();
   return ptr;
  }
 
// Prefix ++.
 
Iter operator++() {
   ptr++;
   return *this;
 
}
 
// Prefix --.
 
Iter operator--() {
   ptr--;
   return *this;
 
}
 
// Postfix ++.
  Iter operator++(int notused) {
   T *tmp = ptr;
  
ptr++;
   return Iter<T>(tmp, begin, end);
  }
  // Postfix --.
  Iter operator--(int notused) {
   T *tmp = ptr;
  
ptr--;
   return Iter<T>(tmp, begin, end);
  }
  // Return a reference to the object at the
  // specified index. Do not allow out-of-bounds
  // access.
  T &operator[](int i) {
  
if( (i < 0) || (i >= (end-begin)) )
     throw OutOfRangeExc();
   return ptr[i];
  }
 
// Define the relational operators.
  bool operator==(Iter op2) {
   return ptr == op2.ptr;
  }
  bool operator!=(Iter op2) {
   return ptr != op2.ptr;
  }
 
bool operator<(Iter op2) {
   return ptr < op2.ptr;
  }
  bool operator<=(Iter op2) {
   return ptr <= op2.ptr;
  }
 
bool operator>(Iter op2) {
   return ptr > op2.ptr;
 
}
 
bool operator>=(Iter op2) {
   return ptr >= op2.ptr;
  }
 
// Subtract an integer from an Iter.
 
Iter operator-(int n) {
   ptr -= n;
   return *this;
 
}
 
// Add an integer to an Iter.
 
Iter operator+(int n) {
   ptr += n;
   return *this;
 
}
 
// Return number of elements between two Iters.
  int operator-(Iter<T> &itr2) {
   return ptr - itr2.ptr;
  }
};
// This class defines an element that is stored
// in the garbage collection information list.
//
template <class T> class GCInfo {
public:
 
unsigned refcount; // current reference count
 
T *memPtr; // pointer to allocated memory
 
/* isArray is true if memPtr points
    to an allocated array. It is false
    otherwise. */
 
bool isArray; // true if pointing to array
 
/* If memPtr is pointing to an allocated
    array, then arraySize contains its size */
  unsigned arraySize; // size of array
 
// Here, mPtr points to the allocated memory.
  // If this is an array, then size specifies
 
// the size of the array.
 
GCInfo(T *mPtr, unsigned size=0) {
   refcount = 1;
   memPtr = mPtr;
   if(size != 0)
    
isArray = true;
   else
     isArray = false;
  
arraySize = size;
  }
};
// Overloading operator== allows GCInfos to be compared.
// This is needed by the STL list class.
template <class T> bool operator==(const GCInfo<T> &ob1,
              
const GCInfo<T> &ob2) {
  return (ob1.memPtr == ob2.memPtr);
}
// GCPtr implements a pointer type that uses
// garbage collection to release unused memory.
// A GCPtr must only be used to point to memory
// that was dynamically allocated using new.
// When used to refer to an allocated array,
// specify the array size.
//
template <class T, int size=0> class GCPtr {
 
// gclist maintains the garbage collection list.
  static list<GCInfo<T> > gclist;
 
// addr points to the allocated memory to which
  // this GCPtr pointer currently points.
  T *addr;
 
/* isArray is true if this GCPtr points
    to an allocated array. It is false
    otherwise. */
 
bool isArray; // true if pointing to array
 
// If this GCPtr is pointing to an allocated
  // array, then arraySize contains its size.
  unsigned arraySize; // size of the array
 
static bool first; // true when first GCPtr is created
 
// Return an iterator to pointer info in gclist.
  typename list<GCInfo<T> >::iterator findPtrInfo(T *ptr);
public:
 
// Define an iterator type for GCPtr<T>.
  typedef Iter<T> GCiterator;
 
// Construct both initialized and uninitialized objects.
  GCPtr(T *t=NULL) {
  
// Register shutdown() as an exit function.
   if(first) atexit(shutdown);
   first = false;
  
list<GCInfo<T> >::iterator p;
  
p = findPtrInfo(t);
  
// If t is already in gclist, then
   // increment its reference count.
   // Otherwise, add it to the list.
   if(p != gclist.end())
    
p->refcount++; // increment ref count
  
else {
     // Create and store this entry.
     GCInfo<T> gcObj(t, size);
     gclist.push_front(gcObj);
   }
  
addr = t;
   arraySize = size;
   if(size > 0) isArray = true;
   else isArray = false;
   #ifdef DISPLAY
    
cout << "Constructing GCPtr. ";
     if(isArray)
       cout << " Size is " << arraySize << endl;
     else
       cout << endl;
   #endif
  }
 
// Copy constructor.
  GCPtr(const GCPtr &ob) {
   list<GCInfo<T> >::iterator p;
  
p = findPtrInfo(ob.addr);
   p->refcount++; // increment ref count
  
addr = ob.addr;
   arraySize = ob.arraySize;
   if(arraySize > 0) isArray = true;
   else isArray = false;
   #ifdef DISPLAY
    
cout << "Constructing copy.";
     if(isArray)
       cout << " Size is " << arraySize << endl;
     else
       cout << endl;
   #endif
  }
 
// Destructor for GCPTr.
  ~GCPtr();
 
// Collect garbage. Returns true if at least
  // one object was freed.
  static bool collect();
 
// Overload assignment of pointer to GCPtr.
  T *operator=(T *t);
 
// Overload assignment of GCPtr to GCPtr.
  GCPtr &operator=(GCPtr &rv);
 
// Return a reference to the object pointed
  // to by this GCPtr.
  T &operator*() {
  
return *addr;
  }
 
// Return the address being pointed to.
  T *operator->() { return addr; }
 
// Return a reference to the object at the
  // index specified by i.
  T &operator[](int i) {
  
return addr[i];
  }
 
// Conversion function to T *.
  operator T *() { return addr; }
 
// Return an Iter to the start of the allocated memory. 
  Iter<T> begin() {
   int size;
  
if(isArray) size = arraySize;
   else size = 1;
  
return Iter<T>(addr, addr, addr + size);
  }
 
// Return an Iter to one past the end of an allocated array.
  Iter<T> end() {
   int size;
  
if(isArray) size = arraySize;
   else size = 1;
   return Iter<T>(addr + size, addr, addr + size);
  }
 
// Return the size of gclist for this type
  // of GCPtr.
  static int gclistSize() { return gclist.size(); }
 
// A utility function that displays gclist.
  static void showlist();
 
// Clear gclist when program exits.
  static void shutdown();
};
// Creates storage for the static variables
template <class T, int size>
  list<GCInfo<T> > GCPtr<T, size>::gclist;
template <class T, int size>
  bool GCPtr<T, size>::first = true;
// Destructor for GCPtr.
template <class T, int size>
GCPtr<T, size>::~GCPtr() {
  list<GCInfo<T> >::iterator p;
 
p = findPtrInfo(addr);
if(p->refcount) p->refcount--; // decrement ref count
 
#ifdef DISPLAY
   cout << "GCPtr going out of scope.\n";
  #endif
 
// Collect garbage when a pointer goes out of scope.  
  collect();
 
// For real use, you might want to collect
  // unused memory less frequently, such as after
  // gclist has reached a certain size, after a
  // certain number of GCPtrs have gone out of scope,
  // or when memory is low.
}
// Collect garbage.  Returns true if at least
// one object was freed.
template <class T, int size>
bool GCPtr<T, size>::collect() {
 
bool memfreed = false;
 
#ifdef DISPLAY
   cout << "Before garbage collection for ";
   showlist();
 
#endif
 
list<GCInfo<T> >::iterator p;
  do {
  
// Scan gclist looking for unreferenced pointers.
  
for(p = gclist.begin(); p != gclist.end(); p++) {
     // If in-use, skip.
     if(p->refcount > 0) continue;
    
memfreed = true;
    
// Remove unused entry from gclist.
     gclist.remove(*p);
     // Free memory unless the GCPtr is null.
     if(p->memPtr) {
       if(p->isArray) {
         #ifdef DISPLAY
           cout << "Deleting array of size "
               
<< p->arraySize << endl;
         #endif
         delete[] p->memPtr; // delete array
      
}
       else {
         #ifdef DISPLAY
           cout << "Deleting: "
               
<< *(T *) p->memPtr << "\n";
         #endif
         delete p->memPtr; // delete single element
      
}
     }
    
// Restart the search.
     break;
   }
 
} while(p != gclist.end());
 
#ifdef DISPLAY
   cout << "After garbage collection for ";
   showlist();
 
#endif
 
return memfreed;
}
// Overload assignment of pointer to GCPtr.
template <class T, int size>
T * GCPtr<T, size>::operator=(T *t) {
 
list<GCInfo<T> >::iterator p;
 
// First, decrement the reference count
  // for the memory currently being pointed to.
  p = findPtrInfo(addr);
  p->refcount--;
 
// Next, if the new address is already
  // existent in the system, increment its
  // count. Otherwise, create a new entry
 
// for gclist.
  p = findPtrInfo(t);
  if(p != gclist.end())
  
p->refcount++;
 
else {
   // Create and store this entry.
   GCInfo<T> gcObj(t, size);
   gclist.push_front(gcObj);
 
}
 
addr = t; // store the address.
 
return t;
}
// Overload assignment of GCPtr to GCPtr.
template <class T, int size>
GCPtr<T, size> & GCPtr<T, size>::operator=(GCPtr &rv) {
 
list<GCInfo<T> >::iterator p;
  // First, decrement the reference count
  // for the memory currently being pointed to.
  p = findPtrInfo(addr);
  p->refcount--;
 
// Next, increment the reference count of
  // the new address.
  p = findPtrInfo(rv.addr);
  p->refcount++; // increment ref count
 
addr = rv.addr;// store the address.
 
return rv;
}
// A utility function that displays gclist.
template <class T, int size>
void GCPtr<T, size>::showlist() {
 
list<GCInfo<T> >::iterator p;
 
cout << "gclist<" << typeid(T).name() << ", "
      << size << ">:\n";
  cout << "memPtr     refcount    value\n";
 
if(gclist.begin() == gclist.end()) {
   
cout << "           -- Empty --\n\n";
   return;
  }
  for(p = gclist.begin(); p != gclist.end(); p++) {
   cout << "[" << (void *)p->memPtr << "]"
     << "       " << p->refcount << "       ";
   if(p->memPtr) cout << " " << *p->memPtr;
   else cout << "  ---";
   cout << endl;
  }
  cout << endl;
}
// Find a pointer in gclist.
template <class T, int size>
typename list<GCInfo<T> >::iterator
 
GCPtr<T, size>::findPtrInfo(T *ptr) {
 
list<GCInfo<T> >::iterator p;
 
// Find ptr in gclist.
  for(p = gclist.begin(); p != gclist.end(); p++)
   if(p->memPtr == ptr)
     return p;
 
return p;
}
// Clear gclist when program exits.
template <class T, int size>
void GCPtr<T, size>::shutdown() {
 
if(gclistSize() == 0) return; // list is empty
 
list<GCInfo<T> >::iterator p;
 
for(p = gclist.begin(); p != gclist.end(); p++) {
   // Set all reference counts to zero
   p->refcount = 0;
  }
 
#ifdef DISPLAY
   cout << "Before collecting for shutdown() for "
   << typeid(T).name() << "\n";
 
#endif
 
collect();
 
#ifdef DISPLAY
   cout << "After collecting for shutdown() for "
        << typeid(T).name() << "\n";
  #endif
}

The garbage collector uses four classes: GCPtr, GCInfo, Iter, and OutOfRangeExc. Before examining the code in detail, it will be helpful to understand the role each class plays.

GCPtr

At the core of the garbage collector is the class GCPtr, which implements a garbage-collection pointer. GCPtr maintains a list that associates a reference count with each piece of memory allocated for use by a GCPtr. In general, here is how it works. Each time a GCPtr is pointed at a piece of memory, the reference count for that memory is incremented. If the GCPtr was previously pointing at a different piece of memory prior to the assignment, the reference count for that memory is decremented. Thus, adding a pointer to a piece of memory increases the memory’s reference count. Removing a pointer decreases the memory’s reference count. Each time a GCPtr goes out of scope, the reference count associated with the memory to which it currently points is decremented. When a reference count drops to zero, that piece of memory can be released.

GCPtr is a template class that overloads the * and –> pointer operators and the array indexing operator [ ]. Thus, GCPtr creates a new pointer type and integrates it into the C++ programming environment. This allows a GCPtr to be used in much the same way that you use a normal C++ pointer. However, for reasons that will be made clear later in this chapter, GCPtr does not overload the ++, – –, or the other arithmetic operations defined for pointers. Thus, except through assignment, you cannot change the address to which a GCPtr object points. This may seems like a significant restriction, but it isn’t because the Iter class provides these operations.

For the sake of illustration, the garbage collector runs whenever a GCPtr object goes out of scope. At that time, the garbage collection list is scanned and all memory with a reference count of zero is released, even if it was not originally associated with the GCPtr that went out of scope. Your program can also explicitly request garbage collection if you need to recycle memory earlier.

GCInfo

As explained, GCPtr maintains a list that links reference counts with allocated memory. Each entry in this list is encapsulated in an object of type GCInfo. GCInfo stores the reference count in its refcount field and a pointer to the memory in its memPtr field. Thus, a GCInfo object binds a reference count to a piece of allocated memory.

GCInfo defines two other fields: isArray and arraySize.If memPtr points to an allocated array, then its isArray member will be true and the length of the array will be stored in its arraySize field.

The Iter Class

As explained, a GCPtr object allows you to access the memory to which it points by using the normal pointer operators * and –>, but it does not support pointer arithmetic. To handle situations in which you need to perform pointer arithmetic, you will use an object of type Iter. Iter is a template class similar in function to an STL iterator, and it defines all pointer operations, including pointer arithmetic. The main use of Iter is to enable you to cycle through the elements of a dynamically allocated array. It also provides bounds checking. You obtain an Iter from GCPtr by calling either begin( ) or end( ), which work much like their equivalents in the STL.

It is important to understand that although Iter and the STL iterator type are similar, they are not the same, and you cannot use one in place of the other.

OutOfRangeExc

If an Iter encounters an attempt to access memory outside the range of the allocated memory, an OutOfRangeExc exception is thrown. For the purposes of this chapter, OutOfRangeExc contains no members. It is simply a type that can be thrown. However, you are free to add functionality to this class as your own applications dictate.

------------------------------------------------------------------

GCPtr is the heart of the garbage collector. It implements a new pointer type that keeps a reference count for objects allocated on the heap. It also provides the garbage collection functionality that recycles unused memory.

GCptr is a template class with this declaration:

template <class T, int size=0> class GCPtr {

GCPtr requires that you specify the type of the data that will be pointed to, which will be substituted for the generic type T. If an array is being allocated, you must specify the size of the array in the size parameter. Otherwise, size defaults to zero, which indicates that a single object is being pointed to. Here are two examples.

GCPtr<int> p; // declare a pointer to a single integer GCPtr<int, 5> ap; // declare a pointer to an array of 5 integers

Here, p can point to single objects of type int, and ap can point to an array of 5 ints.

In the preceding examples, notice that you do not use the * operator when specifying the name of the GCPtr object. That is, to create a GCPtr to int, you do not use a statement like this:

GCPtr<int> *p; // this creates a pointer to a GCPtr<int> object

This declaration creates a normal C++ pointer called p that can point to a GCPtr<int> object. It does not create a GCPtr object that can point to int. Remember, GCPtr defines a pointer type by itself.

Be careful when specifying the type parameter to GCPtr. It specifies the type of the object to which the GCPtr object can point. Therefore, if you write a declaration like this:

GCPtr<int *> p; // this creates a GCPtr to pointers to ints

you are creating a GCPtr object that points to int * pointers, not a GCPtr to ints.

Because of its importance, each member of GCPtr is examined in detail in the following sections.

GCPtr Data Members

GCPtr declares the following data members:

// gclist maintains the garbage collection list.
static list<GCInfo<T> > gclist;
// addr points to the allocated memory to which
// this GCPtr pointer currently points.
T *addr;
/* isArray is true if this GCPtr points
 
to an allocated array. It is false
 
otherwise. */
bool isArray; // true if pointing to array
// If this GCPtr is pointing to an allocated
// array, then arraySize contains its size.
unsigned arraySize; // size of the array
static bool first; // true when first GCPtr is created

The gclist field contains a list of GCInfo objects. (Recall that GCInfo links a reference count with a piece of allocated memory.) This list is used by the garbage collector to determine when allocated memory is unused. Notice that gclist is a static member of GCPtr. This means that for each specific pointer type, there is only one gclist. For example, all pointers of type GCPtr<int> share one list, and all pointers of type GCPtr<double> share a different list. gclist is an instance of the STL list class. Using the STL substantially simplifies the code for GCPtr because there is no need for it to create its own set of list-handling functions.

GCPtr stores the address of the memory to which it points in addr. If addr points to an allocated array, then isArray will be true, and the length of the array will be stored in arraySize.

The first field is a static variable that is initially set to true. It is a flag that the GCPtr constructor uses to know when the first GCPtr object is created. After the first GCPtr object is constructed, first is set to false. It is used to register a termination function that will be called to shut down the garbage collector when the program ends.

The findPtrInfo( ) Function

GCPtr declares one private function: findPtrInfo( ). This function searches gclist for a specified address and returns an iterator to its entry. If the address is not found, an iterator to the end of gclist is returned. This function is used internally by GCPtr to update the reference counts of the objects in gclist. It is implemented as shown here:

// Find a pointer in gclist.
template <class T, int size>
typename list<GCInfo<T> >::iterator
 
GCPtr<T, size>::findPtrInfo(T *ptr) {
 
list<GCInfo<T> >::iterator p;
 
// Find ptr in gclist.
  for(p = gclist.begin(); p != gclist.end(); p++)
   if(p->memPtr == ptr)
     return p;
 
return p;
}

The GCIterator typedef

At the start of the public section of GCPtr is the typedef of Iter<T> to GCiterator. This typedef is bound to each instance of GCPtr, thus eliminating the need to specify the type parameter each time an Iter is needed for a specific version of GCPtr. This simplifies the declaration of an iterator. For example, to obtain an iterator to the memory pointed to by a specific GCPtr, you can use a statement like this:

GCPtr<int>::GCiterator itr;

The GCPtr Constructor

The constructor for GCPtr is shown here:

// Construct both initialized and uninitialized objects. GCPtr(T *t=NULL) {
 
// Register shutdown() as an exit function.
 
if(first) atexit(shutdown);
 
first = false;
  list<GCInfo<T> >::iterator p;
 
p = findPtrInfo(t);
  // If t is already in gclist, then
 
// increment its reference count.
 
// Otherwise, add it to the list.
 
if(p != gclist.end())
  
p->refcount++; // increment ref count
  else {
   // Create and store this entry.
  
GCInfo<T> gcObj(t, size);
  
gclist.push_front(gcObj);
  }
 
addr = t;
 
arraySize = size;
 
if(size > 0) isArray = true;
 
else isArray = false;
 
#ifdef DISPLAY
  
cout << "Constructing GCPtr. ";
   if(isArray)
     cout << " Size is " << arraySize << endl;
   else
     cout << endl;
  #endif
}

GCPtr( ) allows both initialized and uninitialized instances to be created. If an initialized instance is declared, then the memory to which this GCPtr will point is passed through t. Otherwise, t will be null. Let’s examine the operation of GCPtr( ) in detail.

First, if first is true, it means that this is the first GCPtr object to be created. If this is the case, then shutdown( ) is registered as a termination function by calling atexit( ).The atexit( ) function is part of the standard C++ function library, and it registers a function that will be called when a program terminates. In this case, shutdown( ) releases any memory that was prevented from being released because of a circular reference.

Next, a search of gclist is made, looking for any preexisting entry that matches the address in t. If one is found, then its reference count is incremented. If no preexising entry matches t, a new GCInfo object is created that contains this address, and this object is added to gclist.

GCPtr( ) then sets addr to the address specified by t and sets the values of isArray and arraySize appropriately. Remember, if you are allocating an array, you must explicitly specify the size of the array when you declare the GCPtr pointer that will point to it. If you don’t, the memory won’t be released correctly, and, in the case of an array of class objects, the destructors won’t be called properly.

The GCPtr Destructor

The destructor for GCPtr is shown here:

// Destructor for GCPtr.
template <class T, int size>
GCPtr<T, size>::~GCPtr() {
 
list<GCInfo<T> >::iterator p;
 
p = findPtrInfo(addr);
  if(p->refcount) p->refcount--; // decrement ref count
 
#ifdef DISPLAY
   cout << "GCPtr going out of scope.\n";
  #endif
 
// Collect garbage when a pointer goes out of scope. 
  collect();
 
// For real use, you might want to collect
 
// unused memory less frequently, such as after
 
// gclist has reached a certain size, after a
 
// certain number of GCPtrs have gone out of scope,
 
// or when memory is low.
}

Garbage collection takes place each time a GCPtr goes out of scope. This is handled by ~GCPtr( ). First, a search of gclist is made, looking for the entry that corresponds to the address pointed to by the GCPtr being destroyed. Once found, its refcount field is decremented. Next, ~GCptr( ) calls collect( ) to release any unused memory (that is, memory whose reference count is zero).

As the comment at the end of ~GCPtr( ) states, for real applications, it is probably better to collect garbage less often than each time a single GCPtr goes out of scope. Collecting less frequently will usually be more efficient. As explained earlier, collecting each time a GCPtr is destroyed is useful for demonstrating the garbage collector because it clearly illustrates the garbage collector’s operation.

Collect Garbage with collect( )

The collect( ) function is where garbage collection takes place. It is shown here:

// Collect garbage. Returns true if at least
// one object was freed.
template <class T, int size>
bool GCPtr<T, size>::collect() {
bool memfreed = false;
#ifdef DISPLAY
  cout << "Before garbage collection for ";
  showlist();
#endif
list<GCInfo<T> >::iterator p;
do {
  // Scan gclist looking for unreferenced pointers.
 
for(p = gclist.begin(); p != gclist.end(); p++) {
   // If in-use, skip.
   if(p->refcount > 0) continue;
  
memfreed = true;
  
// Remove unused entry from gclist.
   gclist.remove(*p);
  
// Free memory unless the GCPtr is null.
   if(p->memPtr) {
     if(p->isArray) {
       #ifdef DISPLAY
         cout << "Deleting array of size "
             
<< p->arraySize << endl;
       #endif
       delete[] p->memPtr; // delete array
    
}
     else {
       #ifdef DISPLAY
         cout << "Deleting: "
              << *(T *) p->memPtr << "\n";
       #endif
       delete p->memPtr; // delete single element
    
}
   }
  
// Restart the search.
   break;
  }
} while(p != gclist.end());
#ifdef DISPLAY
   cout << "After garbage collection for ";
   showlist();
  #endif
  return memfreed;
}

The collect( ) function works by scanning the contents of gclist, looking for entries that have a refcount of zero. When such an entry is found, it is removed from gclist by calling the remove( ) function, which is a member of the STL list class. Then the memory associated with that entry is freed.

Recall that in C++, single objects are freed by delete, but arrays of objects are freed via delete[ ]. Thus, the value of the entry’s isArray field determines whether delete or delete[ ] is used to free the memory. This is one reason you must specify the size of an allocated array for any GCPtr that will point to one: it causes isArray to be set to true. If isArray is not set correctly, it is impossible to properly release the allocated memory.

Although the point of garbage collection is to recycle unused memory automatically, you can take a measure of manual control if necessary. The collect( ) function can be called directly by user code to request that garbage collection take place. Notice that it is declared as a static function within GCPtr. This means that it can be invoked without reference to any object. For example:

GCPtr<int>::collect(); // collect all unused int pointers

This causes gclist<int> to be collected. Because there is a different gclist for each type of pointer, you will need to call collect( ) for each list that you want to collect. Frankly, if you need to closely manage the release of dynamically allocated objects, you are better off using the manual allocation system provided by C++. Directly calling collect( ) is best reserved for specialized situations, such as when free memory is running unexpectedly low.

GCPtr overloads operator=( ) twice: once for the assignment of a new address to a GCPtr pointer, and once for the assignment of one GCPtr pointer to another. Both versions are shown here:

// Overload assignment of pointer to GCPtr.
template <class T, int size>
T * GCPtr<T, size>::operator=(T *t) {
 
list<GCInfo<T> >::iterator p;
 
// First, decrement the reference count
 
// for the memory currently being pointed to.
 
p = findPtrInfo(addr);
 
p->refcount--;
  // Next, if the new address is already
  // existent in the system, increment its
  // count. Otherwise, create a new entry
  // for gclist.
  p = findPtrInfo(t);
  if(p != gclist.end())
  
p->refcount++;
 
else {
   // Create and store this entry.
   GCInfo<T> gcObj(t, size);
   gclist.push_front(gcObj);
 
}
 
addr = t; // store the address.
 
return t;
}
// Overload assignment of GCPtr to GCPtr.
template <class T, int size>
GCPtr<T, size> & GCPtr<T, size>::operator=(GCPtr &rv) {
 
list<GCInfo<T> >::iterator p;
 
// First, decrement the reference count
  // for the memory currently being pointed to.
  p = findPtrInfo(addr);
  p->refcount--;
 
// Next, increment the reference count of
  // the new address.
  p = findPtrInfo(rv.addr);
  p->refcount++; // increment ref count
 
addr = rv.addr;// store the address.
 
return rv;
}

The first overload of operator=( ) handles assignments in which a GCPtr pointer is on the left and an address is on the right. For example, it handles cases like the one shown here:

GCPtr<int> p;
// ...
p = new int(18);

Here, the address returned by new is assigned to p. When this happens, operator=(T *t) is called with the new address passed to t. First,the entry in gclist for the memory that is currently being pointed to is found, and its reference count is decremented. Next, a search of gclist is made for the new address. If it is found, its reference count is incremented. Otherwise, a new GCInfo object is created for the new address, and this object is added to gclist. Finally, the new address is stored in the invoking object’s addr, and this address is returned.

The second overload of the assignment operator, operator=(GCPtr &rv), handles the following type of assignment:

GCPtr<int> p;
GCPtr<int> q;
// ...
p = new int(88);
q = p;

Here, both p and q are GCPtr pointers, and p is assigned to q. This version of the assignment operator works much like the other. First, the entry in gclist for the memory that is currently being pointed to is found, and its reference count is decremented. Next, a search of gclist is made for the new address, which is contained in rv.addr, and its reference count is incremented. Then the invoking object’s addr field is set to the address contained in rv.addr. Finally, the right-hand object is returned. This allows a chain of assignments to take place, such as:

p = q = w = z;

There is one other important point to make about the way that the assignment operators work. As mentioned earlier in this chapter, it is technically possible to recycle memory as soon as its reference count drops to zero, but doing so puts an extra overhead on each pointer operation. This is why, in the overloaded assignment operators, the reference count for the memory previously pointed to by the left-hand operand is simply decremented, and no further action is taken. Thus, the management overhead associated with actually releasing memory and performing maintenance on gclist is avoided. These actions are deferred to later, when the garbage collector runs. This approach makes for faster runtimes for the code that uses a GCPtr. It also lets garbage collection take place at times that are (potentially) more convenient in terms of runtime performance.

The GCPtr Copy Constructor

Because of the need to keep track of each pointer to allocated memory, the default copy constructor (which makes a bitwise identical copy) cannot be used. Instead, GCPtr must define is own copy constructor, which is shown here:

// Copy constructor.
GCPtr(const GCPtr &ob) {
 
list<GCInfo<T> >::iterator p;
 
p = findPtrInfo(ob.addr);
  p->refcount++; // increment ref count
  addr = ob.addr;
  arraySize = ob.arraySize;
  if(arraySize > 0) isArray = true;
  else isArray = false;
  #ifdef DISPLAY
  
cout << "Constructing copy.";
   if(isArray)
     cout << " Size is " << arraySize << endl;
   else
     cout << endl;
  #endif
}

Recall that a class’ copy constructor is invoked when a copy of an object is required, such as when an object is passed as an argument to a function, when an object is returned from a function, or when one object is used to initialize another. GCPtr’s copy constructor duplicates the information contained in the original object. It also increments the reference count associated with the memory pointed to by the original object. When the copy goes out of scope, this reference count will be decremented.

Actually, the extra work performed by the copy constructor is not usually necessary because the overloaded assignment operators properly maintain the garbage collection list in most cases. However, there are a small number of cases in which the copy constructor is needed, such as when memory is allocated inside a function and a GCPtr to that memory is returned.

The Pointer Operators and Conversion Function

Because GCPtr is a pointer type, it must overload the pointer operators * and –>, plus the indexing operator [ ]. This is done by the functions shown here. Given that it is the ability to overload these operators that makes it possible to create a new pointer type, it is amazing how simple they are.

// Return a reference to the object pointed
// to by this GCPtr.
T &operator*() {
 
return *addr;
}
// Return the address being pointed to.
T *operator->() { return addr; }
// Return a reference to the object at the
// index specified by i.
T &operator[](int i) {
  return addr[i];
}

The operator*( ) function returns a reference to the object pointed to by the addr field of the invoking GCPtr, operator–>( ) returns the address contained in addr, and operator[ ] returns a reference to the element specified by the index. operator[ ] should be used only on GCPtrs that point to allocated arrays.

As mentioned earlier, no pointer arithmetic is supported. For example, neither the ++ nor the –– operator is overloaded for GCPtr. The reason is that the garbage collection mechanism assumes that a GCPtr is pointing to the start of allocated memory. If a GCPtr could be incremented, for example, then when that pointer was garbage collected, the address used with delete would be invalid.

You have two options if you need to perform operations that involve pointer arithmetic. First, if the GCPtr is pointing to an allocated array, you can create an Iter that will let you cycle through that array. This approach is described later. Second, you can convert a GCPtr into a normal pointer by use of the T * conversion function defined by GCPtr. This function is shown here:

// Conversion function to T *.
operator T*() { return addr; }

This function returns a normal pointer that points to the address stored in addr. It can be used like this.

GCPtr<double> gcp = new double(99.2);
double *p;
p = gcp; // now, p points to same memory as gcp
p++; // because p is a normal pointer, it can be incremented

In the preceding example, because p is a normal pointer, it can be manipulated in any way that any other pointer can. Of course, whether such manipulations yield meaningful results is dependent upon your application.

The main advantage of the conversion to T * is that it lets you use GCPtrs in place of normal C++ pointers when working with preexisting code that requires such pointers. For example, consider this sequence:

GCPtr<char> str = new char[80];
strcpy(str, "this is a test");
cout << str << endl;

Here, str is a GCPtr pointer to char that is used in a call to strcpy( ). Because strcpy( ) is expecting its arguments to be of type char *, the conversion to T* inside GCPtr is automatically invoked because, in this case, T is char. The same conversion is automatically invoked when str is used in the cout statement. Thus, the conversion function enables GCPtrs to seamlessly integrate with existing C++ functions and classes.

Keep in mind that the T * pointer returned by this conversion does not participate in or affect garbage collection. Thus, it is possible for the allocated memory to be freed even if a regular C++ pointer is still pointing to it. So, use the conversion to T* wisely––and infrequently.

The begin( ) and end( ) Functions

The begin( ) and end( ) functions, shown next, are similar to their counterparts in the STL:

// Return an Iter to the start of the allocated memory. Iter<T> begin() {
 
int size;
 
if(isArray) size = arraySize;
 
else size = 1;
 
return Iter<T>(addr, addr, addr + size);
}
// Return an Iter to one past the end of an allocated array.
Iter<T> end() {
 
int size;
 
if(isArray) size = arraySize;
 
else size = 1;
  return Iter<T>(addr + size, addr, addr + size);
}

The begin( ) function returns an Iter to the start of the allocated array pointed to by addr. The end( ) function returns an Iter to one past the end of the array. Although there is nothing that stops these functions from being called on a GCPtr that points to a single object, their purpose is to support operations on allocated arrays. (Obtaining an Iter to a single object is not harmful, just pointless.)

The shutdown( ) Function

If a program creates a circular reference of GCPtrs, then when the program ends there will still be dynamically allocated objects that need to be released. This is important because these objects might have destructors that need to be called. The shutdown( ) function handles this task. This function is registered by atexit( ) when the first GCPtr is created, as described earlier. This means that it will be called when the program terminates.

The shutdown( ) function is shown here:

// Clear gclist when program exits.
template <class T, int size>
void GCPtr<T, size>::shutdown() {
 
if(gclistSize() == 0) return; // list is empty
 
list<GCInfo<T> >::iterator p;
 
for(p = gclist.begin(); p != gclist.end(); p++) {
   // Set all reference counts to zero
   p->refcount = 0;
  }
 
#ifdef DISPLAY
   cout << "Before collecting for shutdown() for "
        << typeid(T).name() << "\n";
  #endif
  collect();
 
#ifdef DISPLAY
   cout << "After collecting for shutdown() for "
        << typeid(T).name() << "\n";
  #endif
}

First, if the list is empty, which it will normally be, then shutdown( ) simply returns. Otherwise, it sets to zero the reference counts of the entries that still exist in gclist, and then it calls collect( ). Recall that collect( ) releases any object that has a reference count of zero. Thus, setting the reference counts to zero ensures that all objects will be freed.

Two Utility Functions

GCPtr ends by defining two utility functions. The first is gclistSize( ), which returns the number of entries currently held in gclist. The second is showlist( ), which displays the contents of gclist. Neither of these are necessary for the implementation of a garbage collection pointer type, but they are useful when you want to watch the operation of the garbage collector.

------------------------------------------------------------------

The garbage collection list in gclist holds objects of type GCInfo. The GCInfo class is shown here:

// This class defines an element that is stored
// in the garbage collection information list.
//
template <class T> class GCInfo {
public:
 
unsigned refcount; // current reference count
 
T *memPtr; // pointer to allocated memory
 
/* isArray is true if memPtr points
    to an allocated array. It is false
    otherwise. */
  bool isArray; // true if pointing to array
 
/* If memPtr is pointing to an allocated
    array, then arraySize contains its size */
  unsigned arraySize; // size of array
 
// Here, mPtr points to the allocated memory.
  // If this is an array, then size specifies
  // the size of the array.
  GCInfo(T *mPtr, unsigned size=0) {
  
refcount = 1;
   memPtr = mPtr;
   if(size != 0)
    
isArray = true;
   else
     isArray = false;
  
arraySize = size;
  }
};

As mentioned earlier, each GCInfo object stores a pointer to allocated memory in memPtr and the reference count associated with that memory in refcount. If the memory pointed to by memPtr contains an array, then the length of that array must be specified when the GCInfo object is created. In this case, isArray is set to true, and the length of the array will be stored in arraySize.

GCInfo objects are stored in an STL list. To enable searches on this list, it is necessary to define operator==( ), as shown here:

// Overloading operator== allows GCInfos to be compared.
// This is needed by the STL list class.
template <class T> bool operator==(const GCInfo<T> &ob1,
              
const GCInfo<T> &ob2) {
  return (ob1.memPtr == ob2.memPtr);
}

Two objects are equal only if both their memPtr fields are identical. Depending upon the compiler you are using, other operators may need to be overloaded to enable GCInfos to be stored in an STL list.

------------------------------------------------------------------Iter

The Iter class implements an iterator-like object that can be used to cycle through the elements of an allocated array. Iter is not technically necessary because a GCPtr can be converted to a normal pointer of its base type, but Iter offers two advantages. First, it lets you cycle through an allocated array in a fashion similar to the way in which you cycle through the contents of an STL container. Thus, the syntax for using an Iter is familiar. Second, Iter will not allow out-of-range accesses. Thus, an Iter is a safe alternative to using a normal pointer. Understand, however, that Iter does not participate in garbage collection. Thus, if the underlying GCPtr on which an Iter is based goes out of scope, the memory to which it points will be freed whether or not it is still needed by that Iter.

Iter is a template class defined like this:

template <class T> class Iter {

The type of data to which the Iter points is passed through T.

Iter defines these instance variables:

T *ptr;   // current pointer value
T *end;   // points to element one past end
T *begin; // points to start of allocated array
unsigned length; // length of sequence

The address to which the Iter currently points is held in ptr. The address to the start of the array is stored in begin, and the address of an element one past the end of the array is stored in end. The length of the dynamic array is stored in length.

Iter defines the two constructors shown here. The first is the default constructor. The second constructs an Iter, given an initial value for ptr, and pointers to the beginning and end of the array.

Iter() {
ptr = end = begin = NULL;
  length = 0;
}
Iter(T *p, T *first, T *last) {
  ptr = p;
  end = last;
  begin = first;
  length = last - first;
}

For use by the garbage collector code shown in this chapter, the initial value of ptr will always equal begin. However, you are free to construct Iters in which the initial value of ptr is a different value.

To enable Iter’s pointer-like nature, it overloads the * and –> pointer operators, and the array indexing operator [ ], as shown here:

// Return value pointed to by ptr.
// Do not allow out-of-bounds access.
T &operator*() {
 
if( (ptr >= end) || (ptr < begin) )
   
throw OutOfRangeExc();
  return *ptr;
}
// Return address contained in ptr.
// Do not allow out-of-bounds access.
T *operator->() {
 
if( (ptr >= end) || (ptr < begin) )
   throw OutOfRangeExc();
 
return ptr;
}
// Return a reference to the object at the
// specified index. Do not allow out-of-bounds
// access.
T &operator[](int i) {
 
if( (i < 0) || (i >= (end-begin)) )
   throw OutOfRangeExc();
  return ptr[i];
}

The * operator returns a reference to the element currently being pointed to in the dynamic array. The –> returns the address of the element currently being pointed to. The [ ] returns a reference to the element at the specified index. Notice that these operations do not allow an out-of-bounds access. If one is attempted, an OutOfRangeExc exception is thrown.

Iter defines the various pointer arithmetic operators, such as ++, – –, and so on, which increment or decrement an Iter. These operators enable you to cycle through a dynamic array. In the interest of speed, none of the arithmetic operators perform range checks themselves. However, any attempt to access an out-of-bounds element will cause an exception, which prevents a boundary error. Iter also defines the relational operators. Both the pointer arithmetic and relational functions are straightforward and easy to understand.

Iter also defines a utility function called size( ), which returns the length of the array to which the Iter points.

As mentioned earlier, inside GCPtr, Iter<T> is typedefed to GCiterator for each instance of GCPtr, which simplifies the declaration of an iterator. This means that you can use the type name GCiterator to obtain the Iter for any GCPtr.

-----------------------------------------------------------------

Using a GCPtr is quite easy. First, include the file gc.h. Then, declare a GCPtr object, specifying the type of the data to which it will point. For example, to declare a GCPtr object called p that can point to an int, use this kind of declaration:

GCPtr<int> p; // p can point to int objects

Next, dynamically allocate the memory using new and assign the pointer returned by new to p, as shown here:

p = new int; // assign p the address of an int

You can assign a value to the allocated memory using an assignment operation like this:

*p = 88; // give that int a value

Of course, you can combine the three preceding statements like this:

GCPtr<int> p = new int(88); // declare and initialize

You can obtain the value of the memory at the location pointed to by p, as shown here:

int k = *p;

As these examples show, in general you use a GCPtr just like a normal C++ pointer. The only difference is that you don’t need to delete the pointer when you are through with it. The memory allocated to that pointer will be automatically released when it is no longer needed.

Here is an entire program that assembles the pieces just shown:

#include <iostream>
#include <new>
#include "gc.h"
using namespace std;
int main() {
  GCPtr<int> p;
 
try {
  
p = new int;
  } catch(bad_alloc exc) {
   cout << "Allocation failure!\n";
   return 1;
 
}
 
*p = 88;
 
cout << "Value at p is: " << *p << endl;
 
int k = *p;
 
cout << "k is " << k << endl;
 
return 0;
}

The output from this program with the display option turned on is shown here. (Recall that you can watch the operation of the garbage collector by defining DISPLAY within gc.h.)

Constructing GCPtr.
Value at p is: 88
k is 88
GCPtr going out of scope.
Before garbage collection for gclist<int, 0>:
memPtr        refcount      value
[002F12C0]        0         88
[00000000]        0         ---
Deleting: 88
After garbage collection for gclist<int, 0>:
memPtr        refcount      value
            -- Empty --

When the program ends, p goes out of scope. This causes its destructor to be called, which causes the reference count for the memory pointed to by p to be decremented. Because p was the only pointer to this memory, this operation sets the reference count to zero. Next, p’s destructor calls collect( ), which scans gclist, looking for entries that have a reference count of zero. Because the entry previously associated with p has a reference count of zero, its memory is freed.

One other point: notice that prior to garbage collection, a null pointer entry is also in gclist. This null pointer was created when p was constructed. Recall that if a GCPtr is not given an initial address, the null address (which is zero) is used. Although it is not technically necessary to store a null pointer in gclist (because it is never freed), doing so simplifies other parts of GCPtr because it ensures that every GCPtr has a corresponding entry in gclist.

Handling Allocation Exceptions

As the preceding program shows, because the garbage collector does not change the way that memory is allocated via new, you handle allocation failures in the same way as usual, by catching the bad_alloc exception. (Recall that when new fails, it throws an exception of type bad_alloc.) Of course, the preceding program won’t run out of memory, and the try/catch block isn’t really needed, but a real-world program might exhaust the heap. Thus, you should always check for this possibility.

In general, the best way to respond to a bad_alloc exception when using garbage collection is to call collect( ) to recycle any unused memory and then retry the allocation that failed. This technique is employed by the load-testing program shown later in this chapter. You can use the same basic technique in your own programs.

A More Interesting Example

Here is a more interesting example that shows the effect of a GCPtr going out of scope before the end of the program:

// Show a GCPtr going out of scope prior to the end
// of the program.
#include <iostream>
#include <new>
#include "gc.h"
using namespace std;
int main() {
  GCPtr<int> p;
  GCPtr<int> q;
 
try {
   p = new int(10);
   q = new int(11);
  
cout << "Value at p is: " << *p << endl;
   cout << "Value at q is: " << *q << endl;
  
cout << "Before entering block.\n";
  
// Now, create a local object.
  
{ // start a block
     GCPtr<int> r = new int(12);
     cout << "Value at r is: " << *r << endl;
  
} // end the block, causing r to go out of scope
  
cout << "After exiting block.\n";
  } catch(bad_alloc exc) {
   cout << "Allocation failure!\n";
   return 1;
  }
 
cout << "Done\n";
  return 0;
}

This program produces the following output when the display option is turned on:

Constructing GCPtr.
Constructing GCPtr.
Value at p is: 10
Value at q is: 11
Before entering block.
Constructing GCPtr.
Value at r is: 12
GCPtr going out of scope.
Before garbage collection for gclist<int, 0>:
memPtr      refcount    value
[002F31D8]      0       12
[002F12F0]      1       11
[002F12C0]      1       10
[00000000]      0       ---
Deleting: 12
After garbage collection for gclist<int, 0>:
memPtr      refcount    value
[002F12F0]      1       11
[002F12C0]      1       10
After exiting block.
Done
GCPtr going out of scope.
Before garbage collection for gclist<int, 0>:
memPtr      refcount    value
[002F12F0]      0       11
[002F12C0]      1       10
Deleting: 11
After garbage collection for gclist<int, 0>:
memPtr      refcount    value
[002F12C0]      1       10
GCPtr going out of scope.
Before garbage collection for gclist<int, 0>:
memPtr      refcount    value
[002F12C0]      0       10
Deleting: 10
After garbage collection for gclist<int, 0>:
memPtr      refcount    value
          -- Empty --

Examine this program and its output closely. First, notice that p and q are created at the start of main( ), but r is not created until its block is entered. As you know, in C++, local variables are not created until their block is entered. When r is created, the memory to which it points is given an initial value of 12. This value is then displayed, and the block ends. This causes r to go out of scope, which means that its destructor is called. This causes r’s reference count in gclist to be decremented to zero. Then collect( ) is called to collect garbage.

Because the display option is on, when collect( ) begins, it displays the contents of gclist. Notice that it has four entries. The first is the one that was previously linked to r. Notice that its refcount field is zero, indicating that the memory pointed to by the memPtr field is no longer in use by any program element. The next two entries are still active, and they are linked to p and q. Because they are still in use, the memory they point to is not freed at this time. The final entry represents the null pointer to which p and q originally pointed when they were created. Because it is no longer in use, it will be removed from the list by collect( ). (Of course, no memory is freed when the null pointer is removed.)

Because no other GCPtr points to the same memory as r, its memory can be released, as the Deleting: 12 line confirms. Once this is done, program execution continues after the block. Finally, p and q go out of scope when the program ends and their memory is released. In this case, q’s destructor is called first, meaning that it is collected first. Finally, p is destroyed and gclist is empty.

Allocating and Discarding Objects

It is important to understand that memory becomes subject to garbage collection as soon as its reference count drops to zero (which means that no GCPtr is pointing to it). It is not necessary for the GCPtr that originally pointed to that object to go out of scope. Thus, you can use a single GCPtr object to point to any number of allocated objects by simply assigning that GCPtr a new value. The discarded memory will eventually be collected. For example:

// Allocate and discard objects.
#include <iostream>
#include <new>
#include "gc.h"
using namespace std;
int main() {
  try {
   // Allocate and discard objects.
   GCPtr<int> p = new
int(1);
   p = new int(2);
   p = new int(3);
   p = new int(4);
  
// Manually collect unused objects for
   // demonstration purposes.
   GCPtr<int>::collect();
   
cout << "*p: " << *p << endl;
  } catch(bad_alloc exc) {
    cout << "Allocation failure!\n";
   
return 1;
  }
  return 0;
}

The output from this program, with the display option turned on, is shown here:

Constructing GCPtr.
Before garbage collection for gclist<int, 0>:
memPtr      refcount    value
[002F1310]      1        4
[002F1300]      0        3
[002F12D0]      0        2
[002F12A0]      0        1
Deleting: 3
Deleting: 2
Deleting: 1
After garbage collection for gclist<int, 0>:
memPtr      refcount    value
[002F1310]      1        4
*p: 4
GCPtr going out of scope.
Before garbage collection for gclist<int, 0>:
memPtr      refcount    value
[002F1310]      0        4
Deleting: 4
After garbage collection for gclist<int, 0>:
memPtr      refcount    value
         
-- Empty --

In the program, p, a GCPtr to int, is assigned a pointer to four separate chunks of dynamic memory, each being initialized with a different value. Next, a call is made to collect( ), which forces garbage collection to take place. Notice the contents of gclist: three of the entries are marked inactive, and only the entry that points to the memory that was allocated last is still in use. Next, the unused entries are deleted. Finally, the program ends, p goes out of scope, and the final entry is removed.

Notice that the first three chunks of dynamic memory to which p pointed have reference counts of zero. This is because of the way the overloaded assignment operator works. Recall that when a GCPtr is assigned a new address, the reference count for its original value is decremented. Thus, each time p is assigned the address of a new integer, the reference count for the old address is reduced.

One other point: because p was initialized when it was declared, no null-pointer entry was generated and put on gclist. Remember, a null-pointer entry is created only when a GCPtr is declared without an initial value.

If you are allocating an array using new, then you must tell GCPtr this fact by specifying its size when the GCPtr pointer to that array is declared. For example, here is the way to allocate an array of five doubles:

GCPtr<double, 5> pda = new double[5];

The size must be specified for two reasons. First, it tells the GCPtr constructor that this object will point to an allocated array, which causes the isArray field to be set to true. When isArray is true, the collect( ) function frees memory by using delete[ ], which releases a dynamically allocated array, rather than delete, which releases only a single object. Therefore, in this example, when pda goes out of scope, delete[ ] is used and all five elements of pda are freed. Ensuring that the correct number of objects are freed is especially important when arrays of class objects are allocated. Only by using delete[ ] can you know that the destructor for each object will be called.

The second reason that the size must be specified is to prevent an out-of-bounds element from being accessed when an Iter is used to cycle through an allocated array. Recall that the size of the array (stored in arraySize) is passed by GCPtr to Iter’s constructor whenever an Iter is needed.

Be aware that nothing enforces the rule that an allocated array be operated on only through a GCPtr that has been specified as pointing to an array. This is solely your responsibility.

Once you have allocated an array, there are two ways you can access its elements. First, you can index the GCPtr that points to it. Second, you can use an iterator. Both methods are shown here.

Using Array Indexing

The following program creates a GCPtr to a 10-element array of ints. It then allocates that array and initializes it to the values 0 through 9. Finally, it displays those values. It performs these actions by indexing the GCPtr.

// Demonstrate indexing a GCPtr.
#include <iostream>
#include <new>
#include "gc.h"
using namespace std;
int main() {
 
try {
   // Create a GCPtr to an allocated array of 10 ints.
  
GCPtr<int, 10> ap = new int[10];
  
// Give the array some values using array indexing.
   for(int i=0; i < 10; i++)
     ap[i] = i;
  
// Now, show the contents of the array.
   for(int i=0; i < 10; i++)
     cout << ap[i] << " ";
  
cout << endl;
 
} catch(bad_alloc exc) {
   cout << "Allocation failure!\n";
   return 1;
 
}
 
return 0;
}
The output, with the display option off, is shown here:

0 1 2 3 4 5 6 7 8 9

Because a GCPtr emulates a normal C++ pointer, no array bounds checking is performed, and it is possible to overrun or under run the dynamically allocated array. So, use the same care when accessing an array through a GCPtr as you do when accessing an array through a normal C++ pointer.

Using Iterators

Although array indexing is certainly a convenient method of cycling through an allocated array, it is not the only method at your disposal. For many applications, the use of an iterator will be a better choice because it has the advantage of preventing boundary errors. Recall that for GCPtr, iterators are objects of type Iter. Iter supports the full complement of pointer operations, such as ++. It also allows an iterator to be indexed like an array.

Here is the previous program reworked to use an iterator. Recall that the easiest way to obtain an iterator to a GCPtr is to use GCiterator, which is a typedef inside GCPtr that is automatically bound to the generic type T.

// Demonstrate an iterator.
#include <iostream>
#include <new>
#include "gc.h"
using namespace std;
int main() {
 
try {
   // Create a GCPtr to an allocated array of 10 ints.  
   GCPtr<int, 10> ap = new int[10];
  
// Declare an int iterator.
  
GCPtr<int>::GCiterator itr;
  
// Assign itr a pointer to the start of the array.
  
itr = ap.begin();
  
// Give the array some values using array indexing.
   for(unsigned i=0; i < itr.size(); i++)
     itr[i] = i;
  
// Now, cycle through array using the iterator.
   for(itr = ap.begin(); itr != ap.end(); itr++)
     cout << *itr << " ";
  
cout << endl;
 
} catch(bad_alloc exc) {
  
cout << "Allocation failure!\n";
  
return 1;
  } catch(OutOfRangeExc exc) {
   cout << "Out of range access!\n";
   return 1;
 
}
  return 0;
}

On your own, you might want to try incrementing itr so that it points beyond the boundary of the allocated array. Then try accessing the value at that location. As you will see, an OutOfRangeExc is thrown. In general, you can increment or decrement an iterator any way you like without causing an exception. However, if it is not pointing within the underlying array, attempting to obtain or set the value at that location will cause a boundary error.

Using GCPtr with Class Types

GCPtr is used with class types in just the same way it is used with built-in types. For example, here is a short program that allocates objects of MyClass:

// Use GCPtr with a class type.
#include <iostream>
#include <new>
#include "gc.h"
using namespace std;
class MyClass {
  int a, b;
public:
  double val;
 
MyClass() { a = b = 0; }
 
MyClass(int x, int y) {
   a = x;
   b = y;
   val = 0.0;
 
}
 
~MyClass() {
   cout << "Destructing MyClass(" <<
        a << ", " << b << ")\n";
  }
 
int sum() {
   return a + b;
  }
 
friend ostream &operator<<(ostream &strm, MyClass &obj);
};
// An overloaded inserter to display MyClass.
ostream &operator<<(ostream &strm, MyClass &obj) {
  strm << "(" << obj.a << " " << obj.b << ")";
  return strm;
}
int main() {
  try {
   GCPtr<MyClass> ob = new MyClass(10, 20);
  
// Show value via overloaded inserter.
   cout << *ob << endl;
  
// Change object pointed to by ob.
   ob = new MyClass(11, 21);
  
cout << *ob << endl;
  
// Call a member function through a GCPtr.
   cout << "Sum is : " << ob->sum() << endl;
  
// Assign a value to a class member through a GCPtr.  
   ob->val = 98.6;
   cout << "ob->val: " << ob->val << endl;
  
cout << "ob is now " << *ob << endl;
  } catch(bad_alloc exc) {
  
cout << "Allocation error!\n";
  
return 1;
  }
 
return 0;
}

Notice how the members of MyClass are accessed through the use of the –> operator. Remember, GCPtr defines a pointer type. Thus, operations through a GCPtr are performed in exactly the same fashion that they are with any other pointer.

The output from the program, with the display option turned off, is shown here:

(10 20)
(11 21)
Sum is : 32
ob->val: 98.6
ob is now (11 21)
Destructing MyClass(11, 21)
Destructing MyClass(10, 20)

Pay special attention to the last two lines. These are output by ~MyClass( ) when garbage is collected. Even though only one GCPtr pointer was created, two MyClass objects were allocated. Both of these objects are represented by entries in the garbage collection list. When ob is destroyed, gclist is scanned for entries having a reference count of zero. In this case, two such entries are found, and the memory to which they point is deleted.

The following program shows a larger example that exercises all of the features of GCPtr:

// Demonstrating GCPtr.
#include <iostream>
#include <new>
#include "gc.h"
using namespace std;
// A simple class for testing GCPtr with class types.
class MyClass {
  int a, b;
public:
  double val;
 
MyClass() { a = b = 0; }
 
MyClass(int x, int y) {
   a = x;
   b = y;
   val = 0.0;
 
}
 
~MyClass() {
   cout << "Destructing MyClass(" <<
        a << ", " << b << ")\n";
  }
 
int sum() {
   return a + b;
  }
 
friend ostream &operator<<(ostream &strm, MyClass &obj);
};
// Create an inserter for MyClass.
ostream &operator<<(ostream &strm, MyClass &obj) {
  strm << "(" << obj.a << " " << obj.b << ")";
  return strm;
}
// Pass a normal pointer to a function.
void passPtr(int *p) {
  cout << "Inside passPtr(): "
      << *p << endl;
}
// Pass a GCPtr to a function.
void passGCPtr(GCPtr<int, 0> p) {
  cout << "Inside passGCPtr(): "
      << *p << endl;
}
int main() {
 
try {
   // Declare an int GCPtr.
   GCPtr<int> ip;
  
// Allocate an int and assign its address to ip.
   ip = new int(22);
  
// Display its value.
   cout << "Value at *ip: " << *ip << "\n\n";
  
// Pass ip to a function
   passGCPtr(ip);
  
// ip2 is created and then goes out of scope 
   {
    GCPtr<int> ip2 = ip;
   }
  
int *p = ip; // convert to int * pointer'
  
passPtr(p); // pass int * to passPtr()
  
*ip = 100; // Assign new value to ip
  
// Now, use implicit conversion to int *
   passPtr(ip);
   cout << endl;
  
// Create a GCPtr to an array of ints
   GCPtr<int, 5> iap = new int[5];
  
// Initialize dynamic array.
   for(int i=0; i < 5; i++)
     iap[i] = i;
  
// Display contents of array.
   cout << "Contents of iap via array indexing.\n";
   for(int i=0; i < 5; i++)
    
cout << iap[i] << " ";
   cout << "\n\n";
  
// Create an int GCiterator.
   GCPtr<int>::GCiterator itr;
  
// Now, use iterator to access dynamic array.
   cout << "Contents of iap via iterator.\n";
   for(itr = iap.begin(); itr != iap.end(); itr++)
    
cout << *itr << " ";
   cout << "\n\n";
  
// Generate and discard many objects
   for(int i=0; i < 10; i++)
     ip = new int(i+10);
  
// Now, manually garbage collect GCPtr<int> list.
   // Keep in mind that GCPtr<int, 5> pointers
   // will not be collected by this call.
   cout << "Requesting collection on GCPtr<int> list.\n";
   GCPtr<int>::collect();
  
// Now, use GCPtr with class type.
   GCPtr<MyClass> ob = new MyClass(10, 20);
  
// Show value via overloaded insertor.
   cout << "ob points to " << *ob << endl;
  
// Change object pointed to by ob.
   ob = new MyClass(11, 21);
   cout << "ob now points to " << *ob << endl;
  
// Call a member function through a GCPtr.
   cout << "Sum is : " << ob->sum() << endl;
  
// Assign a value to a class member through a GCPtr. 
   ob->val = 19.21;
   cout << "ob->val: " << ob->val << "\n\n";
  
cout << "Now work with pointers to class objects.\n";
  
// Declare a GCPtr to a 5-element array
   // of MyClass objects.
   GCPtr<MyClass, 5> v;
   // Allocate the array.
   v = new MyClass[5];
  
// Get a MyClass GCiterator.
   GCPtr<MyClass>::GCiterator mcItr;
  
// Initialize the MyClass array.
   for(int i=0; i<5; i++) {
     v[i] = MyClass(i, 2*i);
   }
  
// Display contents of MyClass array using indexing.
   cout << "Cycle through array via array indexing.\n"; 
   for(int i=0; i<5; i++) {
    
cout << v[i] << " ";
   }
   cout << "\n\n";
  
// Display contents of MyClass array using iterator. 
   cout << "Cycle through array through an iterator.\n"; 
   for(mcItr = v.begin(); mcItr != v.end(); mcItr++) {
    
cout << *mcItr << " ";
   }
   cout << "\n\n";
  
// Here is another way to write the preceding loop. 
   cout << "Cycle through array using a while loop.\n"; 
   mcItr = v.begin();
   while(mcItr != v.end()) {
    
cout << *mcItr << " ";
    
mcItr++;
   }
   cout << "\n\n";
  
cout << "mcItr points to an array that is "
        << mcItr.size() << " objects long.\n";
  
// Find number of elements between two iterators. 
   GCPtr<MyClass>::GCiterator mcItr2 = v.end()-2;
   mcItr = v.begin();
   cout << "The difference between mcItr2 and mcItr is "
       
<< mcItr2 - mcItr;
   cout << "\n\n";
  
// Can also cycle through loop like this.
   cout << "Dynamically compute length of array.\n";
   mcItr = v.begin();
   mcItr2 = v.end();
   for(int i=0; i < mcItr2 - mcItr; i++) {
    
cout << v[i] << " ";
   }
  
cout << "\n\n";
  
// Now, display the array backwards.
   cout << "Cycle through array backwards.\n";
   for(mcItr = v.end()-1; mcItr >= v.begin(); mcItr--)
    
cout << *mcItr << " ";
   cout << "\n\n";
  
// Of course, can use "normal" pointer to
   // cycle through array.
   cout << "Cycle through array using 'normal' pointer\n";
   MyClass *ptr = v;
   for(int i=0; i < 5; i++)
    
cout << *ptr++ << " ";
   cout << "\n\n";
  
// Can access members through a GCiterator.
   cout << "Access class members through an iterator.\n";
    for(mcItr = v.begin(); mcItr != v.end(); mcItr++) {
    
cout << mcItr->sum() << " ";
   }
   cout << "\n\n";
  
// Can allocate and delete a pointer to a GCPtr
   // normally, just like any other pointer.
   cout << "Use a pointer to a GCPtr.\n";
   GCPtr<int> *pp = new GCPtr<int>();
   *pp = new int(100);
   cout << "Value at **pp is: " << **pp;
   cout << "\n\n";
  
// Because pp is not a garbage collected pointer,
   // it must be deleted manually.
   delete pp;
 
} catch(bad_alloc exc) {
   // A real application could attempt to free
   // memory by collect() when an allocation
   // error occurs.
   cout << "Allocation error.\n";
 
}
 
return 0;
}

Here is the output with the display option turned off:

Value at *ip: 22
Inside passGCPtr(): 22
Inside passPtr(): 22
Inside passPtr(): 100
Contents of iap via array indexing.
0 1 2 3 4
Contents of iap via iterator.
0 1 2 3 4
Requesting collection on GCPtr<int> list.
ob points to (10 20)
ob now points to (11 21)
Sum is : 32
ob->val: 19.21
Now work with pointers to class objects.
Destructing MyClass(0, 0)
Destructing MyClass(1, 2)
Destructing MyClass(2, 4)
Destructing MyClass(3, 6)
Destructing MyClass(4, 8)
Cycle through array via array indexing.
(0 0) (1 2) (2 4) (3 6) (4 8)
Cycle through array through an iterator.
(0 0) (1 2) (2 4) (3 6) (4 8)
Cycle through array using a while loop.
(0 0) (1 2) (2 4) (3 6) (4 8)
mcItr points to an array that is 5 objects long.
The difference between mcItr2 and mcItr is 3
Dynamically compute length of array.
(0 0) (1 2) (2 4) (3 6) (4 8)
Cycle through array backwards.
(4 8) (3 6) (2 4) (1 2) (0 0)
Cycle through array using 'normal' pointer
(0 0) (1 2) (2 4) (3 6) (4 8)
Access class members through an iterator.
0 3 6 9 12
Use a pointer to a GCPtr.
Value at **pp is: 100
Destructing MyClass(4, 8)
Destructing MyClass(3, 6)
Destructing MyClass(2, 4)
Destructing MyClass(1, 2)
Destructing MyClass(0, 0)
Destructing MyClass(11, 21)
Destructing MyClass(10, 20)

On your own, try compiling and running this program with the display option turned on. (That is, define DISPLAY in gc.h.) Next, walk through the program, matching the output against each statement. This will give you a good feel for the way the garbage collector works. Remember, garbage collection occurs whenever a GCPtr goes out of scope. This happens at various points in the program, such as when a function that receives a copy of a GCPtr returns. In this case, the copy goes out of scope and garbage collection takes place. Also remember that each type of GCPtr maintains its own gclist. Thus, collecting garbage from one list does not cause it to be collected from other types of lists.

The following program load tests GCPtr by repeatedly allocating and discarding objects until free memory is exhausted. When this occurs, a bad_alloc exception is thrown by new. Inside the exception handler, collect( ) is explicitly called to reclaim the unused memory, and the process continues. You can use this same technique in your own programs.

// Load test GCPtr by creating and discarding
// thousands of objects.
#include <iostream>
#include <new>
#include <limits>
#include "gc.h"
using namespace std;
// A simple class for load testing GCPtr.
class LoadTest {
 
int a, b;
public:
 
double n[100000]; // just to take up memory
 
double val;
 
LoadTest() { a = b = 0; }
 
LoadTest(int x, int y) {
   a = x;
   b = y;
   val = 0.0;
 
}
  friend ostream &operator<<(ostream &strm, LoadTest &obj);
};
// Create an inserter for LoadTest.
ostream &operator<<(ostream &strm, LoadTest &obj) {
  strm << "(" << obj.a << " " << obj.b << ")";
  return strm;
}
int main() {
  GCPtr<LoadTest> mp;
  int i;
 
for(i = 1; i < 20000; i++) {
   try {
     mp = new LoadTest(i, i);
  
} catch(bad_alloc xa) {
     // When an allocation error occurs, recycle
     // garbage by calling collect().
     cout << "Last object: " << *mp << endl;
     cout << "Length of gclist before calling collect(): "
          << mp.gclistSize() << endl; 
     GCPtr<LoadTest>::collect();
     cout << "Length after calling collect(): "
         
<< mp.gclistSize() << endl;
   }
  }
 
return 0;
}

A portion of the output from the program (with the display option off) is shown here. Of course, the precise output you see may differ because of the amount of memory available in your system and the compiler that you are using.

Last object: (518 518)
Length of gclist before calling collect(): 518
Length after calling collect(): 1
Last object: (1036 1036)
Length of gclist before calling collect(): 518
Length after calling collect(): 1
Last object: (1554 1554)
Length of gclist before calling collect(): 518
Length after calling collect(): 1
Last object: (2072 2072)
Length of gclist before calling collect(): 518
Length after calling collect(): 1
Last object: (2590 2590)
Length of gclist before calling collect(): 518
Length after calling collect(): 1
Last object: (3108 3108)
Length of gclist before calling collect(): 518
Length after calling collect(): 1
Last object: (3626 3626)
Length of gclist before calling collect(): 518
Length after calling collect(): 1

Some Restrictions

There are a few restrictions to using GCPtr:

  1. You cannot create global GCPtrs. Recall that a global object goes out of scope after the rest of the program ends. When a global GCPtr goes out of scope, the GCPtr destructor calls collect( ) to try to release the unused memory. The trouble is that, depending on how your C++ compiler is implemented, gclist may have already been destroyed. In this case, executing collect( ) will cause a runtime error. Therefore, GCPtr should be used only when creating local objects.
  2. When using dynamically allocated arrays, you must specify the size of the array when you declare a GCPtr that will point to it. There is no mechanism that enforces this, however, so be careful.
  3. You must not attempt to release the memory pointed to by a GCPtr by explicitly using delete. If you need to immediately release an object, call collect( ).
  4. A GCPtr object must point only to memory that is dynamically allocated via new. Assigning to a GCPtr object a pointer to any other memory will cause an error when the GCPtr object goes out of scope because an attempt will be made to free memory that was never allocated.
  5. It is best to avoid circular pointer references for reasons described earlier in this chapter. Although all allocated memory is eventually released, objects containing circular references remain allocated until the program ends, rather than being released when they are no longer used by another program element.

Some Things to Try

It is easy to tailor GCPtr to the needs of your applications. As explained earlier, one of the changes that you might want to try is collecting garbage only after some metric has been reached, such as gclist reaching a certain size, or after a certain number of GCPtrs have gone out of scope.

An interesting enhancement to GCPtr is to overload new so that it automatically collects garbage when an allocation failure occurs. It is also possible to bypass the use of new when allocating memory for a GCPtr and use factory functions defined by GCPtr instead. Doing this lets you carefully control the dynamic allocation of memory, but it makes the allocation process fundamentally different than it is for C++’s built-in approach.

You might want to experiment with other solutions to the circular reference problem. One way is to implement the concept of a weak reference, which does not prevent garbage collection from occurring. You would then use a weak reference whenever a circular reference was needed.

Perhaps the most interesting variation on GCPtr is found in Chapter 3. There, a multithreaded version is created in which garbage collection takes place automatically, when free CPU time exists.


"C++" 카테고리의 다른 글
  • C++ Preprocessor: The Code in the Middle (0)2007/07/30
  • Multithreading in C++ (0)2007/07/30
  • A Simple Garbage Collector for C++ (0)2007/07/30
  • Function Pointers (1)2007/07/30
  • DLL Conventions: Issues and Solutions (0)2007/07/27
2007/07/30 10:18 2007/07/30 10:18
Posted by webdizen
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3107

Leave your greetings.

[로그인][오픈아이디란?]

Programming/C++2007/07/30 10:16

Function Pointers

출처 : http://www.devarticles.com/c/a/cplusplu ··· art-1%2F

In this first of a three-part series of articles, J. Nakamura covers regular function pointers. These pointers allow you to access the code you write for your applications in the form of functions.

Introduction

In a previous series of articles named “Pointer Perfect,” I looked at how data stored in computer memory is accessible through pointers. The code you write for your application in the form of functions (these can be global or class member functions) is very much accessible through pointers in the same way, and I will introduce you to the syntax and usage of these function pointers in this article.

Function pointers are great for implementing callbacks and are very useful when you want to introduce late binding in your application. They help you parse configuration files, help you read protocols and can help you store a function call in an object when you want to delay or record the call (very useful in an undo/redo system).

Most C++ books treat function pointers superficially, which is very unfortunate; maybe this is because their syntax looks a bit weird when you first start using them. I hope to be able to convince you that the right function pointer at the right moment in the right place will make your code clearer, cleaner and easier to understand. Once you get past the odd syntax and come to understand the power of calling functions through pointers, a whole new level of abstraction will open up to you.

A Quick Example

There is nothing like a quick example to take you straight to the heart of a topic. Here is a simple code example that uses a function pointer.

// main.cpp

#include

int IncOne(int var) {

return ++var;

}

int main(int argc, char *argv[]) {

// creating pointer to function

int (*inc_one)(int) = &IncOne;

// use the function the standard way

int result = IncOne(1);

(void)printf(“result is: %d\n”, result);

// utilize the function pointer

result = inc_one(1);

(void)printf(“result is: %d\n”, result);

return 0;

}

When you run this simple example, you will notice that both times the result printed is "2" -- as expected, of course. So what has the compiler come up with for us? Let’s take a peek into the debugger at the value stored in inc_one.

int (*inc_one)(int) = &IncOne;

00411A9E mov dword ptr [inc_one],offset IncOne (411523h)

It holds the address 0x00411523, and looking at the memory at that position we find the following line:

0041126C jmp IncOne (411A40h)

Our code is ordered to jump to 0x411A40, where we of course will find the code of our IncOne() function:

int IncOne(int var)

{

00411A40 push ebp

00411A41 mov ebp,esp

...

}

So what does the assembly in the debugger look like when we make a normal function call to IncOne() ?

int result=IncOne(1);

00411AA5 push 1

00411AA7 call IncOne (41126Ch)

Hold on! The debugger tells us that we go to 0x0041126c, where we are made to jump to IncOne (0x00422A40). So what happens when we call the function using the function pointer inc_one?

result=inc_one(1);

00411AC3 mov esi,esp

00411AC5 push 1

00411AC7 call dword ptr [inc_one]

Exactly the same thing! The content of inc_one is 0x0041126C, which, when called, makes our code jump to IncOne (0x00422A40), where the function is executed. This basically covers the whole concept of function pointers. All that is left now are the many (strange) forms in which they can appear.

The function pointer in our quick example points to the address of an ordinary C function. There are no special considerations for us to make in this case.

The code you compile to create your executable takes up a certain space in the computer's memory, where your data and functions will reside during its execution. Just like the data accessible through pointers, we have seen that functions are accessible in the very same way. The only thing a compiler has to know is how to interpret the address to which a pointer is pointing; this is where things can become slightly confusing, as we run into the often odd syntax of function pointers.

Defining and Using Regular Function Pointers

Just like regular pointers, function pointers are variables that need to be declared and defined. In the quick example you saw that inc_one is our function pointer variable, holding the pointer to a function that returns an int and that takes an int as a parameter.

To give you another example, if you declare the following function:

void AnotherFunction(std::string const &argument, float value);

Then you would assign the address of this function to a function pointer variable like this:

void (*fncptr)(std::string const&,float) = &AnotherFunction;

Hopefully you can see a pattern emerging here. The function declaration is basically being repeated, with the function name replaced by a "*varName," and only the types of the parameters are named. The retrieval of the address of a function works just like the address retrieval of a variable; you can use the address dereference operator "&."

Personally, I find the above demonstrated way of defining a function pointer a bit hard to read. When you need more variables to hold pointers to the same function, it can become quite laborious and sensitive to typing errors, too. Not to mention the fact that when the function definition changes, you would have to chase down and change every function pointer pointing to the changed function as well.

So here we will find the use of the typedef keyword very useful:

typedef int (*FncPtr)(int);

When we stick to the example, we can replace the add_one definition with:

FncPtr inc_one = &IncOne;

So you see that it is really easy to declare function pointers. Just as with normal pointers, you either use the pointer directly or you use it by dereferencing the pointer explicitly:

int result1 = inc_one(1);

int result2 = (*inc_one)(1);

Making the function call through a pointer or just the plain way will yield not very noticeable differences. It's quite nice to be able to abstract a function call into a variable, isn’t it?

Passing a function pointer as an argument to a function is quite essential when you want to implement callback functions. Although we won’t be looking at the implementation and usage of callback functions until later on, let’s take a look at the syntax right now.

The benefit of passing a function pointer as an argument to another function lies in the fact that this allows us to dictate which function that other function has to execute. There is nothing really different from what we have seen before, except for the fact that it looks slightly different.

// a raw declaration

void foo(int (*FPtr)(char const*));

// using a typedef

typedef int (*FuncPtr)(char const*);

void foo(FuncPtr fptr);

Did you recognize the function passed in through a pointer to foo? It is a function that takes a char const pointer as an argument and returns an int.

int func(char const *param);

Another tricky syntax is the raw declaration of a function that returns a function pointer.

// a raw declaration

int (*getFuncPtr1(int ID))(char const*);

// using a typedef

typedef int (*FuncPtr)(char const*);

FuncPtr getFuncPtr2(int ID);

It is pretty clear that the usage of a typedef clears things up a lot in this case. Did you notice that getFuncPtr1 returns a pointer to the same function (func) we had actually already declared before?

Function Pointer Arrays

Since they are stored as variables, it is possible to have an array of function pointers -- but mixing the two guarantees a confusing syntax when you are going to do it all "raw." The beauty of function pointer arrays comes from the fact that you can bind functions to indices, which in turn could come from an enumeration you declared earlier on. This way you can create statements like: int res = (*opArray[PLUS])(a,b);.

// a raw declaration

int (*funcArray1[10])(char const*);

// using a typedef

typedef int (*FuncPtr)(char const*);

FuncPtr funcArray2[10];

// an example assignment

funcArray1[0] = funcArray2[5] = &func;

// example usage

int result1 = (*funcArray1[0])(“a test”);

int result2 = (*funcArray2[5])(“and then some”);

Let’s try to make this even more interesting and take a look at how we define pointers to templated functions. First we need a templated function to point at:

template T Inc(T var) {

return ++var;

}

Unfortunately the C++ standard doesn’t allow us to make template declarations in the scope of function bodies, so we cannot define a pointer to a templated function like this in main():

template T (*IncPtr)(T) = &Inc;

It is important to understand how compilers instantiate templates as well. They only instantiate templated code when it is needed, and they will not even detect a syntax error if the templated function is never called (a lot of clever and tricky meta template programming makes good use of this compiler behavior). It doesn’t make sense, and hopelessly complicates things for compiler builders, when you try to declare a templated pointer at a point where the compiler is hard at work instantiating code (as in function bodies). When you make a function call, data is going to be moved onto the memory stack, and the compiler needs to know which parameters to use. By trying to template the pointer, we are denying it that very information, and it won’t be able to continue the compilation.

So what is it we can do with the templated function Inc() we have? When we tell the compiler how we are planning to use the function, defining a function pointer to it works fine:

int main(int argc, char *argv[]) {

// Use Inc to increase an integer

int (*incN)(int) = &Inc;

int nres = incN(1);

(void)printf(“result is: %d\n”, nres);

// Use Inc to increase a float

float (*incF)(float) = &Inc;

float fres = incF(1.f);

(void)printf(“result is: %f\n”, fres);

}

When you run this example you will see that it works fine.

So is there no way we can delay the type definition of the function we want to define a function pointer to? Well, there is one location where this is possible: inside a templated function:

template

T Foo(T var) {

T (*incPtr)(T) = &Inc ;

return incPtr(var); }

Adding the following line to our example shows that it works fine:

int foores = Foo (1);

(void)printf(“result is: %d\n”, result);

When you think about it, it actually makes sense, because by the time Foo is instantiated, typename T is known by the compiler, and it has no problem instantiating the function pointer to Inc.

There is much more to function pointers than actually meets the eye. In the next article I will take a look at class member function pointers and place them in the same context I have done with the regular function pointers in this article. Finally, in the third article you will find a callback implementation example, function pointers mixed with calling conventions and an introduction to C++ functors.

In the previous article, Jun Nakamura introduced you to the use of regular function pointers, but when you write C++ code, you will be interested in C++ class member function pointers too. It is time to look at how to declare pointers to the members of classes you write for your applications.

Class Member Function Pointer Syntax

The declaration of a C++ member function pointer is a bit trickier than the regular ones we have seen so far and I have to say they look pretty strange as well.

You might wonder what the use of a member function pointer is. One of the uses I have found for it is in a protocol definition I wrote for a server application. The protocol is described in a static struct, where string messages are tied to pointers to the member functions in the protocol class that can handle specific messages. This ties the abstract knowledge of the messages to the lower functionality in the protocol class. Here is a section of that code:

static struct RQRSTable {
  unsigned int         privileges;
  ProtocolImpl::RQType requestType; 
  HandleFunc           function;
  string               header;
} rqrsTable[] =
{
// LOGIN -------------------------------------------------
// (CLT)RQ::LOGIN::<`username(str)`>
//   example RQ::LOGIN::`Jun`
  {
   Read | Write | Admin,  // privileges
   ProtocolImpl::Request, // request type 
   &ProtocolImpl::login,  // function
   “CLT::RS::LOGIN”       // header
  },
  ...
};

By now you should be able to see that &ProtocolImpl::login is the function pointer stored in this struct. It describes which function has to be called for which header. A simple loop in a parser function can extrapolate the header from a message, look it up in the struct and execute the attached function by making a call to
rqrsTable[idx].function().

Though a function pointer to a member function looks a bit more complex, it shouldn’t be too difficult to understand how they work now. Here is the function pointed at by the example:

bool ProtocolImpl::login(string const &msg, RQRSTable const *rt);

And this is the typedef I used to define the function pointer as used in the example struct:

typedef bool (ProtocolImpl::*HandleFunc)
                 (string const&, struct RQRSTable*);

It all doesn’t look that different from the regular function pointer syntax when you take a closer look at it. The main difference lies in the fact that you have to specify the class scope of the function pointer, so that the compiler understands that the function pointer is aimed at the member of a class.

Of course there are more ways to declare a member function in a class, so let’s use this simple class definition to try them out:

class MyClass {
public:
  MyClass() : m_value(0) {}
  void SetValue(int value) { m_value=value; }
  int GetValue() const { return m_value; }
  inline void InlineFoo() { (void)printf(“inlined\n”); }

static void foo(char const *msg, int value)
  { (void)printf(“%s %d\n”, msg, value); }
private:
  int m_value;
};

The function pointer comparable to the one used in the example struct is one that points to void MyClass::SetValue(int):

// raw declaration and assignment
void (MyClass::*svalue)(int) = &MyClass::SetValue;

// utilizing a typedef
typedef void (MyClass:*SValue)(int);
SValue svalue = &MyClass::SetValue;

So you see that the function pointer to a member function only needs ‘MyClass::’ in front of the variable name to declare it (which is probably the reason why it instinctively looks a bit weird). Member functions can have more identities than this one so let’s see how we define pointers to the other functions.

// raw declaration and assignment
int (MyClass::*gvalue)() const = &MyClass::GetValue;

// utilizing a typedef
typedef void (MyClass::GValue)() const;
GValue gvalue = &MyClass::GetValue;

A const member function pointer is defined the same way we have been declaring function pointers up to now: just add const.

// raw declaration and assignment
void (MyClass::*inlfoo)() = &MyClass::InlineFoo;

// utilizing a typedef
typedef void (MyClass::*InlFoo)();
InlFoo infloo = &MyClass::InlineFoo;

The function pointer above may come as a small surprise, because you would expect the code of an inlined member function to be pasted into where we normally would find the function call. However when you declare a member function pointer to an inlined function, the compiler creates a body for that function so that it can be executed when you call that member function pointer.

// raw declaration and assignment
void (*foo)(char const*, int) = &MyClass::foo;

// utilizing a typedef
typedef void (*FooPtr)(char const*, int);
FooPtr foo = &MyClass::foo;

Can you guess which member function is being pointed at by FooPtr without having to go back to the declaration of MyClass?

It is pointing at: ‘static void foo (char const * msg, int val );’ Again this may come a bit unexpected, but pointers to static member functions are declared just like the regular function pointers. And when you think about it… static member functions can be used just like regular functions, with them main difference being the fact that you have to provide the class scope (‘MyClass::’) when calling them.

Defining member function pointers is one thing; now let’s see how we use them. There are some specifics you have to be aware of when calling member functions in objects. Let’s write some code that tests the above defined member function pointers.

int main (int argc, char *argv[]) { =
  MyClass myclass;
  SValue svalue = &MyClass::SetValue;
  (myclass.*svalue)(0x1234);

  GValue gvalue = &MyClass::GetValue;
  int res = (myclass.*gvalue)();
  (void)printf(“result is 0x%d\n”, res);

  InlineFoo inlinefoo = &MyClass::InlineFoo; 
  (myclass.*inlinefoo)();

  FooPtr fooptr = &MyClass::foo;
  Fooptr(“result is: “, (myclass.*gvalue)());
}

It will generate the following output:

result is 0x00001234
inlined
result is: 4660

When you plan to use a member function pointer you will need an object of the class you are going to use it on. The syntax boils down to (myobject.*fncptr)(), where <MYOBJECT.> is the name of the instantiated class and <*FNCPTR> is the name of the used member function pointer.

How do member function pointers hold up in the face of Polymorphism? Will the function in a derived class be executed when we use a member function pointer containing the address of the baseclass function? Let me show you what I am getting at.

A typical way to make good use of polymorphism is to have a collection of base class pointers that are pointing to instantiated derived objects. To do this, you have to create the derived object and use the pointer to its baseclass:

class Base {
public:
virtual ~Base()=0;
virtual int foo() const=0;
};

class Deriv1 : public Base { public: int foo() { return 0; } };
class Deriv2 : public Base { public: int foo() { return 1; } };

// somewhere in your code
Base *ptr1 = new Deriv1;
int result = ptr1->foo(); // result will be 0

One of the great benefits of polymorphic classes is that you don’t have to be familiar with their implementation, only with their interface as it is declared in the baseclass. In the example above we know that there is a function called foo() but we don’t have to know what it exactly does. This can be a very useful concept for example in a game where there could be a list of game objects that all implement a run() function. The main game loop can call this run function on every game object, without having to know it is dealing with physics, A.I. or particle system objects. This also means you can add new game objects that implement different behaviors (and/or visuals) without having to make changes to the main code base.

So basically my question is: “When I use a member function pointer to Base::run() and use it on a Base* pointer that actually points to a derived game object… will the code in that game object be executed or will the code in the base object be executed?” A flipside question is whether it is possible to combine the member function pointer to Base::run() on a pointer (or reference) to a derived game object… thus not directly on Base itself. Let’s try this out with our Base and Deriv1 and Deriv2 example.

typedef int (Base::*BaseFooPtr)();
// ... later
BaseFooPtr fooptr = &Base::foo;
// ... test1 – try the BaseFooPtr on Base* to a Deriv1 object.
Base *baseptr = new Deriv1;
int result1 = (baseptr->*fooptr)();
// ... test2 – try the BaseFooPtr on a Deriv2 object.
Deriv2 myDeriv2;
Int result2 = (myDeriv2.*fooptr)();

Running this code you will find that member function pointers behave the same way you would expect normal function calls to behave in the face of polymorphism… answering both of my questions. The results are the same as when we wouldn’t have used a member function pointer but the regular functions calls instead. Thus result1 is 0 and result2 is 1!

It is very well possible that you want to pass a member function pointer to a function or return one from a function. We have seen that all we need to do is add the ‘MyClass::’ scope in front of the variable name, so lets look at how this translates to function parameters and return values. Let’s place the example function we’ve used before into class MyClass:

int MyClass::func(char const *param);

Here are some functions that accept a pointer to this member function:

// a raw declaration
void foo(int (MyClass::*FPtr)(char const*));

// using a typedef
typedef int (MyClass::*FuncPtr)(char const*);
void foo(FuncPtr fptr);

If you compare these function declarations with the ones in the previous article, you will see that indeed all you need to add is ‘MyClass::’. Declaring a function that returns a function pointer, we do the exact same thing:

// a raw declaration
int (MyClass::*getFuncPtr1(int ID))(char const*);

// using a typedef
typedef int (MyClass::*FuncPtr)(char const*);
FuncPtr GetFuncPtr2(int ID);

I honestly think that the raw declaration above (possibly combined with a couple of function pointers as parameters) is a good candidate for usage in an obfuscated C++ coding contest.

Do you understand the following function declaration?

char const* const ( MyClass::*FuncAt(
   void (MyClass::*Fptr1)(char, char),
   float (MyClass::*Fptr2)(int) ))(char const*, int);

Lets not even go there ;-) !

Member Function Pointer Arrays

Similar to the previous member function pointers described before; in order to create member function pointer arrays, we only need to add the class scope to the regular function pointer array declaration.

// a raw declaration
int (MyClass::*funcArray1[10])(char const*);

// using a typedef
typedef int (MyClass::*FuncPtr)(char const*);
FuncPtr funcArray2[10];

// initializing ome
funcArray1[0] = &MyClass::func;

// and finally making use of it
MyClass myClass;
int result1 = (MyClass.*funcArray1[0])(“a test”);

Now that we have seen the many forms function pointers can take, it is quite understandable that many C++ programming books treat this topic superficially. I hope I was able to shed some light in this rather shaded corner of the language and in the concluding article, I will show you the effect calling conventions have on function pointers, provide you with an example and introduce you to C++ functors.

In previous articles, Jun Nakamura introduced using regular function pointers, C++ class member function pointers, and declaring pointers to the members of classes. In this article, he writes about calling conventions, callback functions, and begins to talk about using functors.

There are many programming languages out there and a lot of them come with very useful libraries and APIs. It is possible to tie all these different libraries (coded in different languages) together in your main application.

Maybe you want to develop the user interface in Visual Basic, need a performance sensitive engine coded in C and C++ and have some legacy functionality of which nobody understands the implementation anymore running in Fortran. As you might have guessed, I have actually worked for a company that strung applications together that used libraries like these.

Different languages use different calling conventions because there are many ways to implement function calls on a computer. The main issue here is the order in which function parameters are passed to the function when it is called. These parameters are put onto the memory stack so that the function that is about to execute has access to them. The next issue is whether the caller or the called function will clean up these parameters.

Though I don’t want to go too deep into calling conventions, stacks etcetera, let me give you a small overview.

Alright, here is an overview of how different some Windows calling conventions can be (__pascal, __fortran and __syscall are no longer supported):

__cdecl
Argument Passing: right to left
Stack Maintenance: Calling function pops arguments from the stack
Name Decoration (C only): ‘_’ prefixed to function names (e.g. ‘_foo’)

__stdcall
Argument Passing: right to left
Stack Maintenance: Called function pops its own arguments from the stack
Name Decoration (C only): ‘_’ prefixed to function name, ‘@’ appended followed by the number of decimal bytes in the argument list. (e.g. ‘_foo@10’)

__fastcall (applies to Intel CPUs, this is the default calling convention for Borland)
Argument Passing: First two DWORD arguments are passed in ECX and EDX, the rest is passed right to left
Stack Maintenance: Called function pops its own arguments from the stack
Name Decoration (C only): ‘@’ is prefixed to the name, ‘@’ appended followed by the number decimal bytes in the argument list. (e.g. ‘@foo@10’)

thiscall (used automatically by C++ code)
Argument Passing: ‘this’ pointer put in ECX, arguments passed right to left
Stack Maintenance: Calling function pops arguments from the stack
Name Decoration: None

You see that there are many ways leading to Rome, and this is just on a Windows machine. What is interesting for this article though, is how we declare a function pointer to the following function definition:

void __stdcall Convention(int value, char const *string)
{
  (void)printf(“%d, ‘%s’\n”, value, string);
}

Again, the answer here is simple; we simply integrate the calling convention we need into the function pointer declaration:

void (__stdcall *ConvPtr)(int, char const*) = &Convention;

The principle remains the same whether it is a member function, a function argument, etc. Just bolt what you need into the declaration.

When your linker is giving you errors that it cannot resolve external symbols while you know that you are linking with the correct libraries; be sure to check which calling conventions are being used.

One of the great concepts of function pointers is that of the callback function. As you have seen it is pretty easy to transform the identity of a function into a pointer, which can point to different implementations of a function with that same identity (they all return the same type and accept the same type of parameters in the same sequence). This means that as long as you create a function that returns the right argument and takes the right argument(s), you can insert your own functionality into other people’s code.

An excellent example of this is a library that can read and render Macromedia Flash files, but has to run on a lot of different platforms including game consoles. Since memory management is radically different from console to console (there are no malloc or free functions standard on a Nintendo GameCube), the library allows you to register your own memory allocation and deallocation callback functions with it. Whenever it needs a piece of memory, it will use these callbacks, using your memory manager that implements the functionality they need. The library can focus on what it does best: playing .swf files and you can take care of the platform specific details without having to make any changes to the library.

Let’s look at an example a bit closer to home: qsort. By now you should be able to understand the following function declaration:

void qsort( void *base
             , size_t num
             , size_t width
             , int (__cdecl *comp)
                      (const void* e1, const void* e2)
          );

You will find it in <stdlib.h> and it is a quicksort implementation that sorts the items in base for you, according to your specification. Now how do you specify it to order your items when it doesn’t know anything about the type of the elements? Simple: provide it with a callback function that tells qsort how to compare two elements with each other.

The base address of your list of items is given to qsort through the void *base parameter. It can then figure out how to iterate through the items in the list, because you tell it how many items it contains with num and how big each item is with width. Finally the callback function you provide tells qsort whether an item is smaller than, bigger than, or equal to the other item.

The following example (for Microsoft only, because of the use of _stricmp which is not part of the ANSI standard) reads and sorts the command line parameters using qsort and a callback function.

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int comp(void const *arg1, void const *args) {
  // case insensitive comparison
  return _stricmp( *(char**)arg1, *(char**)arg2);
}

int main(int argc, char const *argv[])
{
  // remove application name from argv
  ++argv;
  --argc;
  // sort remaining arguments
  qsort((void*)argv, (size_t)argc, sizeof(char*), comp);
  // display result
  for (int idx=0; idx<argc; ++idx)
   (void)printf(“%s “, argv[idx]);
  (void)printf(“\n”);
}

Callback functions can also be very useful when you want to bind a token to a function. One of the examples I have shown you before in the previous article, contains a struct named RQRSTable. It contains a member named function and another one named header. Declared in a .cpp source file, structs like these allow you to configure your token/function binding centrally in the source code. They generally take on the following form:

static struct <StructName> {
  <member1 type> <member1 name>;
  ... 
  <memberN-1 type> <memberN-1 name>;
  <memberN type> <memberN name>;
} <array name>[] =
{ { <member value>, ..., <memberN-1 value>, <memberN value> }
, { <member value>, ..., <memberN-1 value>, <memberN value> }
...
};

Now that you have a static data object that can describe which function has to execute when a certain token is used. This can be very useful when you are parsing messages, upon which action has to be taken. If you don’t want to iterate to the array sequentially, you can always store the data in a hash table (like std::map) upon initialization and use the token as a key.

The function pointers in these structs are also callback functions. This is because, just like qsort, the function using them binds data to functions dynamically at runtime. This level of abstraction is very powerful because it allows you to create generic functions like qsort, platform independent libraries etc.

C++ has more tricks up its sleeve though. Allow me introduce you to functors.

Regular function pointers are quite easy to use, but as soon as you start using member function pointers, you need an object to use them on. They are no good to you otherwise.

You can also store both of them in a Functor and treat that functor object as a replacement function instead.

How does an object behave like a function? You overload operator () in a polymorphic MyFunctor base class from which we will implement derived functors. This way our main application can deal with MyFunctor without having to know what kind of specific derived implementations we have provided it. You can give it functors with extra debugging functionality or performance enhanced ones etc. The application won’t care; it will just call operator ().

// the abstract baseclass
class MyFunctor {
public:
  virtual ~MyFunctor() {}
  virtual int operator ()(char const *str)=0;
};

// derived implementation
template <class T> class DerFunctor : public MyFunctor {
public:
  DerFunctor(T* pObj, int (T::*funcPtr)(const char*))
     : m_funcPtr(funcPtr), m_pObject(pObj) {}

  int operator ()(char const *str) {
     return (m_pObject->*m_funcPtr)(str);
  }
private:
  int (T::*m_funcPtr)(const char*); // member func.ptr.
  T* m_pObject;                     // ptr. to object.
};

The constructor of DerFunctor takes a pointer to the object it can use the member function pointer on, which it receives as an argument as well. When we call operator () on an instantiated DerFunctor, it then executes that member function pointer on that object. Easy, isn’t it?

Here is how we can make good use of this.

class Dummy1 {
public:
  int Run(char const *str) { /*...*/ return 0; }
};

class Dummy2 {
public:
  int Run(char const *str) { /*...*/ return 1;
};

int main (int argc, char const *argv[]) {
  // here are the objects
  Dummy1 dummy1; 
  Dummy2 dummy2;
  // here are the functors
  DerFunctor<Dummy1> functor1(&dummy1, &Dummy1::Run);
  DerFunctor<Dummy2> functor2(&dummy2, &Dummy2::Run);
  // put them in an array to make it more interesting \
  MyFunctor* vtable[] = { &functor1, &functor2 };

  // make the call
  int res1 = functor1(“this is a test”);
  int res2 = (*vtable[1])(“this is a test”);
}

This functor example is extremely simplified and there is a lot more to be discussed. The base class I provided here for example, isn’t really generic and you won’t be able to reuse it really. Since these articles were about function pointers I will save the functors for another one.


"C++" 카테고리의 다른 글
  • Multithreading in C++ (0)2007/07/30
  • A Simple Garbage Collector for C++ (0)2007/07/30
  • Function Pointers (1)2007/07/30
  • DLL Conventions: Issues and Solutions (0)2007/07/27
  • More on Handling Basic Data Types (0)2007/07/27
2007/07/30 10:16 2007/07/30 10:16
Posted by webdizen
Tags C++Keyword C++, Function Pointer, 함수 포인터
No Trackback 1 Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3106

Leave your greetings.

  1. 임영택

    C++ 콜백함수에 대한 자료를 찾다가 여기까지 오게 되었네요. 좋은 자료 잘 보았구요.
    링크된 사이트에서 Part1/2/3 모두 잘 읽었습니다. SETI에 대해서 다시한번 더 알게 되었구요.
    모쪼록 성공하시길 빕니다.

    2008/01/21 01:55 [ Permalink : Modify/Delete : Reply ]
[로그인][오픈아이디란?]

Programming/C++2007/07/27 17:56

DLL Conventions: Issues and Solutions

출처 : http://www.devarticles.com/c/a/cplusplu ··· art-i%2F

DLL Conventions: Issues and Solutions, Part I
(Page 1 of 4 )

Have you ever experienced compatibility issues between Dynamic Link Libraries developed using different tools? It is not irrational to use a DLL developed with one tool in a different tool; sometimes there are very good reasons to do so. This first article in a three-part series explaining how to resolve the issues presents the problem plainly.

Abstract

As a developer we often find ourselves surrounded by problems that actually have nothing to do with our productivity or even the project we're working on. One of them is the issue of compatibility between the Dynamic Link Libraries developed using different tools. Recently, while working on a project, I faced this problem, and in this article I'd  like to present my analysis. We'll also talk about writing DLLs that are easy for us develop and don't cause others to face similar problems when they try to use them.

I assume that you're familiar with using Microsoft Visual C++ and Borland C++ Builder, because these are the tools I'll use to expose the nature of problem and the solution.

What is the need to do so anyway?

Some people may argue that they can't imagine why someone would ever need to use DLLs written using one tool in some different tool. An elegant solution would be to have a special build of the DLL for every other tool that can consume the DLL; but, like many other people, I don't find it appealing. Moreover, there are circumstances where you're bound to work on these issues, such as:

  • You do not have the source to the DLL, or even the import library. Don't know what an import library is? Don't worry; we'll talk about that shortly.

  • You have existing applications to maintain and, to add a specific feature, you company has purchased a library. The source is, of course, not available, and you still need to figure out how to call the function when linker gives up saying there are unresolved externs.

I can add more to this list but I believe the above two points give you the idea. So let's see what the problem is, apart from understanding the terminology used in the context and finding a working solution for all of these problems. What I explain here will be specific to the tools I've been using, but I'm sure problems related to other tools won't be much different.

DLL Conventions: Issues and Solutions, Part I - A simple problem
(Page 2 of 4 )

Before we move on I'll explain a generic problem that you might have experienced first hand in your career as a developer, but dismissed because someone did that for you, or you took an entirely different approach to solve that problem.

Say you just downloaded the PDFLib PDF library and purchased the license to it. Prior to your purchase, you tried the sample applications and they all just compiled happily. Now when the development started, you realized that you'll be developing the applications that use the library in Borland C++ Builder 6.0 rather than Visual C++ 6.0, in which all the samples were written and compiled. If you've been through such a situation you can surely appreciate the description of the problem above.

Let's now analyze and find the cause and solution to the problem. If you're reading this just to gain some knowledge, then you'll surely find the basic terminology discussion beneficial.

Some Terms to Know

A Dynamic Link Library (DLL) is a file that contains C Style functions or whole C++ classes that you can use for developing your applications to lessen your development effort and to save time when testing problems in a normal SDLC.

Any function that is visible outside the DLL and is intended for use by external client applications is said to be an export from the DLL. Consequently any client using this function from the DLL is said to have an import from the DLL. Sometimes we call all exported C Style functions and Classes to be exported symbols as a collection.

An import library is a file with a .lib extension and provides the compiler with enough information to resolve function call references if you choose to link implicitly at compile time. You may choose to load the DLL at runtime (using LoadLibrary API) and resolve and call functions then, by locating them in the DLL (using GetProcAddress API).

The import library is the key element if you're linking implicitly, and in most cases this poses a greater problem, as we'll see shortly. Also required is the header file and the DLL you're trying to use.

So now let's recap all we've discussed until now and then we shall move to analysis of the problem. While using third party DLLs in an environment which poses certain problems in the usage, we start with three elements (please note that we're stretching only to implicit linking, and that too with DLLs exporting C style functions, and explicit linking is not in the scope of our discussion right now. But we shall discuss it in the next article of this series):

  1. The DLL built with any tool and exporting C Style functions only.

  2. A header file that provides prototypes to all the functions