Click here to Skip to main content
15,895,794 members
Articles / Programming Languages / VC++

The "SMAT" Subsystem for C++

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
6 Apr 2020CPOL21 min read 5.3K   151   8   1
A surprisingly easy way to avoid memory leaks with new/delete allocations in C++
In this article, you will learn an easy way to avoid memory leaks that happen with new/delete allocations in C++. After a brief introduction to Sensible Memory Allocation Tracking and getting some background perspective, you will see how to use SMAT and how it works. You will also take a look at caveats and extensions.

Hard at Work..

Introduction to Sensible Memory Allocation Tracking

In this article, I'll be presenting the "SMAT" subsystem, where "SMAT" stands for "Sensible Memory Allocation Tracking". It consists of exactly one mandatory C++ source file, one mandatory C++ header file, and two entirely optional C++ header files. Adding the two aforementioned mandatory C++ source files to any (reasonably recent) Visual C++ project basically gives you the power to automatically track all memory allocations made (in that project) with the (global) C++ "new" and "delete" operators.

It does this by providing macro replacements for the standard C++ "new" and "delete" operators, which you can then use instead of those operators to automatically track all memory allocations made.

The idea of replacing the new and delete operators in C++ with macros designed to assist in tracking memory allocations is probably not a new idea (excuse the pun). However, since I have yet to see anything out there (on the internet) that can do that, which is also quick and easy to install and use, unobtrusive, free, and downloadable, I decided to develop my own, which is what I humbly present here. Should it turn out that someone else has developed something similar to what I present here (and I don't know that they haven't), then let's call this a "revisit" of that (old?) idea.

It's very easy to install and use. Just include the aforementioned two mandatory C++ source files ("SMAT.cpp" and "SMAT.h") into any Visual C++ project that you have, then globally #define the manifest constant, "USE_SMAT", to turn it on (in your DEBUG releases only), and you're done (except for the new and delete replacements, which I'll get into later).

The system is self-contained, user-extensible, thread-safe, and has zero impact on production releases. I'm calling it the "Sensible Memory Allocation Tracking" subsystem (or "SMAT" for brevity) because it's based on the very simple idea that tracking anything is something that computers do exceedingly well, so getting your computer to track your own program's memory allocations just makes sense on a very basic level (i.e. it is "sensible"). In fact the idea works so well that, used properly, the SMAT subsystem can virtually eliminate memory leaks created from (the use of) the new and delete operators in any Visual C++ project.

But that's really all it does. It will not, for example, help with memory leaks created through the use of Microsoft's COM/DCOM interface(s), because COM/DCOM has its own (very specific) memory allocation rules, functions, and methods that are separate and distinct from the C++ new and delete operators. It therefore won't help with "Smart pointers" either (which aren't really pointers, they're just big, fat classes).

However, having said that, you can use this subsystem in a C language program (as opposed to C++) to track allocations made by, for example, malloc(), by simply making a few small changes to SMAT's single mandatory C++ header file. I haven't done that, and in fact, don't plan to explore it any further in this article, but it really wouldn't be all that difficult to do.

Some Background Perspective

As you know, when you (your program actually, but let's not get semantic) allocate memory with the "new" operator, you get back a pointer (a real one;) to the allocated memory. Then you use that allocated memory (through the pointer) as you see fit until you no longer need it, at which point you're supposed to free it with the "delete" operator. All very well and good (in theory).

The problem with that scenario is that even if you're very good at keeping track of all the myriad bits of memory that your program allocates, nothing and no one is going to tell you that you "missed one" - that you failed to free one or more (or thousands) of chunks of memory. The memory itself won't balk - it blithely assumes that you must still need it. The program won't object either - it only cares if you use an invalid pointer. Even the OS doesn't care, unless you end up allocating all available memory, which almost never happens (especially with 64-bit compilations).

Enter the SMAT subsystem. SMAT will tell you if you failed to free an allocated chunk of memory. Or two. Or a hundred. It will also alert you if you attempt to free an unallocated pointer, but that's more perk than benefit.

How to Use SMAT

As previously mentioned, the SMAT subsystem provides macro replacements for the "new" and "delete" operators that are (almost) as easy to use as the operators themselves. Specifically:

C++
TypeNme *Objct  = new TypeNme;           // <== becomes ==>  
                                         // TypeNme *Objct  = NEW_SINGLE( TypeNme );
TypeNme *Array  = new TypeNme[ Num ];    // <== becomes ==>  
                                         // TypeNme *Array  = NEW_ARRAY( TypeNme, Num );
ClassNme *Klass = new ClassNme( p, q );  // <== becomes ==>  ClassNme *Klass = 
                                         // NEW_OBJECT( ClassNme, ClassNme( p, q ) );

and (assuming the above has been executed):

C++
delete Objct;       // <== becomes ==>  FREE_SINGLE( TypeNme, Objct );
delete [] Array;    // <== becomes ==>  FREE_ARRAY( TypeNme, Array );
delete Klass;       // <== becomes ==>  FREE_SINGLE( ClassNme, Klass );

As can be seen from the above, the major difference between using the SMAT macros instead of what they replace, is that the SMAT macros that free allocated memory require a type be specified, whereas the standard C++ delete operator does not. So the SMAT macros are, in fact, slightly harder to use when freeing memory. I believe however, that the benefits of using SMAT far outweigh that particular additional hardship. Besides, having to specify the type of memory you're freeing when freeing it may actually improve clarity, for both the reader and writer of the program.

So how does using these macros (instead of their corresponding native C++ operators) help eliminate memory leaks?

Good question. The answer is contained in another of SMAT's macros, called "CHK4LEFTOVERS". This macro acts like a C function that takes exactly one parameter, and returns void (no return value). What this macro does is produce a text file detailing all of the memory blocks allocated by your program, but never freed. The contents of that text file will look something like this:

Memory allocated at line 396 in file, "C:\Team_A\Dev\Saucer\flight.cpp" was never freed.
Memory allocated at line 37 in file, "C:\Team_A\Dev\Saucer\rotation.cpp" was never freed.
Memory allocated at line 396 in file, "C:\Team_A\Dev\Saucer\flight.cpp" was never freed.
Memory allocated at line 3457 in file, "C:\Team_B\Dev\Saucer\escape.cpp" was never freed.
Memory allocated at line 157 in file, "C:\Team_B\Dev\Saucer\MOI.cpp" was never freed.
Memory allocated at line 3457 in file, "C:\Team_B\Dev\Saucer\escape.cpp" was never freed.

In the output text file (shown above), which we'll call the "SMAT Report" for lack of a better name, there will be exactly one line of text for each pointer that your program allocated with the new operator, but never freed with the delete operator. From there, you can look up the code where each pointer was allocated to investigate further.

The "CHK4LEFTOVERS" macro takes one parameter: the full path name of the desired SMAT Report (as a C++ TCHAR string), which it will either create or overwrite every time it is called. However, if all allocated pointers were properly freed at the time the "CHK4LEFTOVERS" macro is invoked, then no SMAT Report is produced at all (no leftover pointers, no report).

Last (and quite possibly least) in the SMAT subsystem's arsenal of C++ macros, is the INIT_SMAT() macro. Its purpose is, of course, to initialize the SMAT subsystem. It must be called before any other SMAT macro can be used, and takes one parameter, which is a function pointer to a user-defined callback function. The specific type of the function pointer, "ON_SCREW_UP", is defined in the SMAT.h file as:

C++
typedef void (*ON_SCREW_UP)( const TCHAR *SrcFile, unsigned LineNum, void *Not_Allocated );

This function pointer parameter can be NULL on entry to the INIT_SMAT() macro. If not NULL, the function pointed to by this parameter will be called by the SMAT subsystem if or when your program attempts to free a pointer that was never allocated, which is always a fatal error. The function itself should therefore not return. If it does, an exception will be generated immediately after it returns anyway. The first two parameters passed to this function will specify where in the source code the "screw up" occurred, and the last parameter specifies the unallocated pointer that was erroneously being freed (by that code).

Should you decide to define such a callback function, be sure to make its definition contingent on whether or not the manifest constant, "USE_SMAT", is #defined, or else you'll end up with a superfluous function definition in your production code. Not catastrophic given that it will probably be optimized out by the linker anyway, but bad form nonetheless.

More Detailed Usage

Implementing SMAT - Step 1

To summarize then, once you've successfully added both of the SMAT subsystem's mandatory C++ source files to your C++ project (and I'll assume you know how to do that), the very first thing that your program has to do is call the INIT_SMAT() macro. This should be done as early as possible, and only needs to be done once. Do not call it more than once. If you download the demo project associated with this article, you'll see the following good (but not great) example of how to call the INIT_SMAT() macro:

C++
#ifdef USE_SMAT		// <== Yes, you should use this to conditionalize 
                    // all of your SMAT supporting code:

/*------------------------------------------------------------------------*/
extern __declspec( noreturn ) void On_Bad_Free( const TCHAR *, unsigned, void * );
/*------------------------------------------------------------------------*/

static __declspec( noreturn ) void On_Bad_Free( const TCHAR *SrcFile, unsigned LineNum, void * )
{
    static TCHAR BadFreeMsg[] =
    _T("Attempt to delete an unallocated pointer at line %u in file, \"%s\".");

    TCHAR ErrMsg[ Elems( BadFreeMsg ) + 1024 ];
    _stprintf( ErrMsg, BadFreeMsg, LineNum, SrcFile );
    GenError( ERR_KERNEL, ErrMsg );
}

#endif	/* USE_SMAT */

/*------------------------------------------------------------------------*/

static bool InitApplication( HINSTANCE hInstance )
{
    AppInst = hInstance;
    AppDlg = GetDesktopWindow();	// <== ..until we have an App window, 
                                    // use the desktop's for error reporting..

    if ( ERROR_HAPPENED ) return( false );					// <==	Returns here after 
                                                            // a fatal error..
    if ( !INIT_SMAT( On_Bad_Free ) ) GenWinKernelErr();		// <==	Initializes the SMAT 
                                                            // (or does nothing).

    return( true );
}

In the above, if the "USE_SMAT" manifest constant is not #defined, such as in any and all of your production code, then the INIT_SMAT() macro call will resolve to a constant Boolean true, so that the whole line of code will be optimized out. Actually, the above is not the best example, because it can obviously be further optimized for production releases, but it's only a demo, so clarity took precedence over efficiency in this particular instance.

Implementing SMAT - Step 2

With that out of the way, the second thing you'll have to do to implement the SMAT subsystem into your C++ project is decide where and when your program will be calling the aforementioned "CHK4LEFTOVERS" macro. You can call it anytime after the INIT_SMAT() macro call, but the best idea, at least at first, is to make the call the last line of code in your program, as in the following code snippet taken from this article's downloadable demo project:

C++
int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE, LPTSTR, int nCmdShow )
{
    if ( InitApplication( hInstance ) ) MakeTopLvlDlg( IDD_Main_Dlg, MainDlg, nCmdShow );
    CHK4LEFTOVERS( _T("C:\\EgSMAT.dbg") );    // <== Iff USE_SMAT is #defined, 
                                              // this line will generate a
    return( 0 );                              // text file listing all allocated memory 
                                              // that was NOT freed..
}

Implementing SMAT - Step 3

Last, but certainly not least, you'll have to replace all memory allocations that your program makes with the global C++ new operator, with their corresponding SMAT subsystem macro equivalents. If an array is being allocated, then the NEW_ARRAY() macro should be used. If not, then the NEW_SINGLE() macro should be used, unless it's a class. If a class is being allocated, then the NEW_OBJECT() macro should be used. Unless it's Tuesday. Just kidding.

You'll also have to replace all memory de-allocations (i.e., uses of the global C++ delete operator) with their corresponding SMAT macro equivalents. If freeing an array, then the FREE_ARRAY() macro should be used, otherwise the FREE_SINGLE() macro should be used.

At this point, it's very important to note that although you really only have to replace the memory allocations that you want to track (as opposed to all of them) with their SMAT macro equivalents, the allocations that you do choose to track with SMAT must be released with a SMAT macro, or erroneous results will ensue. Specifically, if you use, for example, the NEW_SINGLE() macro to allocate some memory, then use the C++ delete operator directly to free that allocated memory, the "CHK4LEFTOVERS" macro will report that the allocated memory was never freed. Basically that's because the SMAT subsystem's "NEW_" family of macros will add the newly allocated pointer to an internal list, and the SMAT subsystem's "FREE_" family of macros will then remove it from that same list. This is, in fact, how the SMAT subsystem discerns "leftover" allocations (aka: memory leaks).

So initially, it might be a good idea to "test out" the SMAT subsystem on only a few memory allocations, just to get a feel for it. As you get more comfortable with the use of the SMAT macros, you'll probably want to use them for more and more allocations, preferably leading to the use of SMAT for all of your C++ allocations. The system was, in fact, designed for that, which is why it can handle tens of thousands of memory allocations with relative ease.

How It Works

As alluded to earlier, how the SMAT subsystem works is exceedingly simple in principle. When a memory allocation is requested, SMAT will allocate the memory, but before it returns the pointer, it will save a copy of that pointer to an internal sorted list (of allocated pointers), along with some relevant information about where, in the source code, the allocation was made, which l like to call the allocation's "meta-data". When a request is then made to free allocated memory, SMAT will first remove the specified pointer from its internal list, then free the memory. Finally, when the "CHK4LEFTOVERS" macro is called, that function simply has to report the aforementioned "relevant information" (contained in the "meta-data") of all the pointers that still exist in (i.e., have yet to be removed from) its internal sorted list of allocated pointers.

Okay, so maybe not so "exceedingly simple" when elucidated in English, but still. Allocate means add to a list, delete means remove from that list, and if that list is not empty when the "CHK4LEFTOVERS" macro is called, then there are memory leaks, which are reported in the SMAT Report. So the goal is to run your DEBUG executables to completion without generating any SMAT Reports. Then your code can be said to be free of memory leaks (at least, all those tracked with SMAT).

Actually, the SMAT subsystem maintains two lists of allocated pointers, not just one - one for array allocations, and one for all other, non-array allocations (which I simply refer to as "single" allocations, as in "NEW_SINGLE()" and "FREE_SINGLE()"). All that really means is that you have to know whether or not you're dealing with an array when allocating and freeing memory, requisite information not dissimilar to that already required by the C++ new and delete operators themselves.

Internally, each list of allocated pointers is a Red-Black tree, which is basically a load-balancing binary sorted linked list optimized for fast lookups. Looked at in another light, you could also describe a Red-Black tree as a completely self-contained in-memory database kernel, but let's not wax poetic just yet. Almost all of the program code that maintains the Red-Black tree structure was taken from the public domain (I modified it a bit), but specific credit for providing same, and making it all at least sound like it should be comprehensible, is due, with much gratitude, to this excellent URL Resource, which I would highly recommend to anyone wishing to investigate further the mechanics of Red-Black trees.

The lists themselves are opaque to the End-User, and in fact, don't even have to be Red-Black trees if you don't want the extra memory overhead (which is tiny anyway, but whatever). If your project globally #defines the manifest constant, "NO_RED_BLACK", then the internal allocated pointer lists revert to simple singly-linked lists, and all lookups are done sequentially (which is much, much slower).

Caveats and Extensions

As with any API, there will be caveats. Don't do this:

C++
if ( TCHAR *Ptr = NEW_ARRAY( TCHAR, 100 ) )
{
    Use_The( Ptr );
    FREE_SINGLE( TCHAR, Ptr );
}

If you do, the aforementioned ON_SCREW_UP function will be called, and your program will terminate.

Which is really just an overly convoluted way of pointing out that the SMAT macro usage protocol is very simple: if you allocate with NEW_ARRAY(), you have to de-allocate with FREE_ARRAY(); if you allocate with NEW_SINGLE() or NEW_OBJECT(), you have to de-allocate with FREE_SINGLE() - so not exactly rocket science.

Another, somewhat more subtle mistake that a SMAT subsystem novice might make is illustrated by the following code snippet:

C++
if ( TCHAR *Ptr = NEW_ARRAY( TCHAR, 100 ) )
{
    Use_The( Ptr );
    FREE_ARRAY( TCHAR *, Ptr );
}

In the above, the type specified in the call to the FREE_ARRAY() macro is wrong. It specifies the type of the pointer being freed, not the type of the object or objects being freed. All types passed as parameters to SMAT macros are assumed to specify the type of the object or objects being allocated or freed. Since this is also true of the C++ new operator, this should come as no surprise.

SMAT Extensions

As mentioned briefly in the Introduction (above), the SMAT subsystem is "user-extensible". This is not so much a "feature" of SMAT, as it is an absolute necessity. Allow me to explain.

Ubiquitous to C++ programming is the use of structures and classes to organize data, most of the memory for which is quite often allocated at runtime. These extremely common blocks of memory very often contain pointers to other allocated blocks of memory (other structures), each of which, in turn, can contain pointers to yet more allocated blocks of memory, and so on. The point is, allocating a structure or class is almost never a simple matter of allocating a single block of memory. Which is why competent C++ programmers will very often create either a class constructor (for classes) or a standalone function (for structures), to do the bulk of the work of allocating all (or most) of the myriad blocks of memory required by a particular class or structure.

Now let's suppose that your program routinely allocates (and de-allocates) thousands of such structures (or classes), all through the use of a very elegant "allocation function" and an equally elegant "de-allocation function". When the program finishes, you want to know whether or not all of the structures that were allocated with your "allocation function" were freed using your "de-allocation function". And if you missed one, you want to know where it was allocated in your program code, so you can track its lifetime more closely. Fair enough.

But without SMAT Extensions, you can't do that. If the structure in question contains, for example, twenty pointers to allocated memory, all allocated using SMAT macros, and your program fails to free one or more of these structures, the SMAT Report will contain twenty-one memory leaks for each structure not de-allocated (assuming the structure itself was allocated), which is not what you want. Worse than that, though, is that all 21 memory leaks (per structure) will be reported by SMAT as having originated from inside your (very elegant) "allocation function", not from the program code that called that function, which is what you really want.

To solve this problem, you have to use SMAT Extensions. Fundamentally, a SMAT Extension is a function that you define (or already have defined) that allocates one or more memory blocks (with SMAT macros), not unlike the "allocation function" previously discussed. However, special-purpose SMAT macros have to be employed when declaring, defining, and calling a SMAT Extension, because it has to work in both debug and release versions (assuming that your debug versions #define "USE_SMAT" and your release versions don't).

A SMAT Extension can return anything you want it to return, and take as many parameters as you like. SMAT Extensions that don't take parameters, however, have a different protocol than those that do. So you might say that, from a usage standpoint, there are two types of SMAT Extension: those that take parameters, and those that don't.

Declaring a SMAT Extension

Let's say you already have an existing allocation function that takes no parameters, and is declared as:

C++
extern SUM_BLK_TYPE *New_SumBlk( void );

If you want to make this function into a SMAT Extension, the declaration needs to be changed to:

C++
extern SUM_BLK_TYPE *New_SumBlk( SMX_VOID );

On the other hand, if your existing allocation function does take parameters, such as in:

C++
extern SUM_BLK_TYPE *New_SumBlk( CNTRL *, unsigned );

then the declaration would instead need to be changed to this:

C++
extern SUM_BLK_TYPE *New_SumBlk( SMX_PARAMS( CNTRL *, unsigned ) );

So the rule is: if the function declaration has a non-void formal parameter list, then they all must be passed (unaltered) to the "SMX_PARAMS" macro, as shown in the example above. This includes variadic functions as well. Otherwise, the "SMX_VOID" macro must be used.

Defining a SMAT Extension

Regardless of whether or not your SMAT Extension takes any parameters, its definition must be immediately preceded by:

C++
#include "Inherit_On.h"

This tells the SMAT subsystem that all SMAT allocation macro and Extension calls issued in the following SMAT Extension definition(s) will "inherit" the allocation meta-data that specifies the source code that called the SMAT Extension, not the source code that called the SMAT allocation macro or Extension (which is the default behavior). If that sounds a little confusing, don't worry; you don't have to know how it works to use it.

Additionally, immediately after the body of the SMAT Extension has been specified, the following preprocessor directive must be issued:

C++
#include "Inherit_Off.h"

This will, of course, undo whatever the "Inherit_On.h" header file did, thereby reverting all SMAT allocation macros back to their default behavior. It can be included after a single SMAT Extension definition, or after a set of SMAT Extension definitions, as long as they are all defined contiguously in the module, and preceded by the aforementioned directive to include the "Inherit_On.h" header file.

Once that is taken care of, you still have to change the declarative part of each SMAT definition (slightly), the specifics of which follow.

Suppose you already have an existing allocation function that takes no parameters, where the declarative part of its definition (i.e., its "head") is:

C++
SUM_BLK_TYPE *New_SumBlk()

If you want to make this function into a SMAT Extension, the definition needs to be changed to:

C++
SUM_BLK_TYPE *New_SumBlk( SMX_META )

On the other hand, if your existing allocation function does take parameters, such as in:

C++
SUM_BLK_TYPE *New_SumBlk( CNTRL *Cntrl, unsigned NumItems )

then the definition "head" would instead need to be changed to this:

C++
SUM_BLK_TYPE *New_SumBlk( SMX_PARAMS( CNTRL *Cntrl, unsigned NumItems ) )

So the rule here is pretty much the same as the aforementioned rule for SMAT Extension declarations, except that functions that take no parameters must use the "SMX_META" macro, as opposed to the "SMX_VOID" macro used for declarations.

Calling a SMAT Extension

Assuming the aforementioned example SMAT Extension has been declared (in scope) and defined, you'll want to call it at some point.

If our previous example SMAT Extension does not take any parameters, a call to it would look like this:

C++
SUM_BLK_TYPE *Gimme = New_SumBlk( SMX_XTEND );

Alternatively, if our example SMAT Extension does take parameters, the call would need to look like this:

C++
SUM_BLK_TYPE *Gimme = New_SumBlk( SMX_PASS( &MyCntrl, 42 ) );

So the long and the short of it is that if the SMAT Extension takes parameters, then they all have to be passed to the SMX_PASS() macro (verbatim), the output of which is then passed to the SMAT Extension. If the SMAT Extension takes no parameters, the SMX_XTEND macro must be used instead, as illustrated above.

A Final Word on SMAT Extensions

Although the mechanics of transforming one or more of your own allocation functions into "SMAT Extensions" may seem, at first, to be more trouble than it's worth, it really isn't. That's because, once the transformation is complete, you'll have an allocation function that:

  1. will execute unchanged in your production releases, and
  2. will be considered by the SMAT subsystem to be a low-level "block allocator", just like its own NEW_SINGLE() and NEW_ARRAY() macros, and therefore
  3. will correctly set allocation meta-data to the program code that called the function, which is what you'll need to know if the allocations are never freed.

Should you wish to see a really good example of an actual, working SMAT Extension, this article's downloadable demo project contains two, both of which are variadic.

The first one is called, "MkeNewStrng", which allocates and returns a TCHAR string containing the concatenation of all the TCHAR strings passed to it, except for the last one, which must always be NULL. The second one is called, "EssPrintEff", and basically does what the _sctprintf() function does, but also allocates and returns a pointer to the formatted result.

It is, on the other hand (I have lots of hands), quite possible to get quite a lot out of the SMAT subsystem without capitalizing on any of SMAT's Extension capabilities. Your choice.

Package Contents

The entire subsystem is contained in just four (4) MSVS-compatible C++ source files, two of which are tiny, and only necessary if you're creating one or more SMAT Extension functions (see above). The four files are:

  1. SMAT.h - Add to your stdafx.h if you use that, or else anything that requires it.
  2. SMAT.cpp - All source code in this module is contingent on the manifest constant, "USE_SMAT", so if not #defined, will actually produce nothing.
  3. Inherit_On.h - Very tiny, single-purpose file, whose sole use is as described in the Defining a SMAT Extension section (above).
  4. Inherit_Off.h - Ditto the above.

The latter two files in the above list only exist because I couldn't figure out how to make a macro that can invoke preprocessor directives. I'm sure I'll get smacked in the head for saying that by some preprocessor guru out there, who'll say "you idiot, you could have used Blah dee Blah for that!!". Whatever. Open to suggestion.

Final Thoughts

When I first thought of writing this article, and subsequently realized that a "demo" project would probably be an advisable thing to include, I knew I didn't want to provide one that was useless for any other purpose. So this article's downloadable demo project, which I've called "EgSMAT", actually does something quasi-useful: it lists all of the files, on any accessible drive in your system, that cannot be opened for reading using conventional methods. The results are usually... somewhat interesting.

Lastly, having used this debugging subsystem, in various forms, for well over a dozen years, I've become convinced of its utility, so I thought I'd share it with the world, so I could become rich and famous and buy a house next to Yvonne Strahovski. I have to admit though, that might not happen... overnight..;>

History

  • 5th April, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
BugCurrent Errata Pin
Doctor Autonomy7-Apr-20 11:08
Doctor Autonomy7-Apr-20 11:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.