tags:

views:

141

answers:

3

I have a recurring problem with a statically linked library I've written (or in some cases, code was accumulated from open sources).

This library, MFC Toolbox Library by name, has a lot of free functions, classes, and so on which support MFC programming, Win32 API programming, as well as the venerable C-library and newer C++ standard library.

In short, this is a working library with tools that apply to my daily work, that I've accumulated over more than a decade, and is indispensable to our products. As such, it has a rich mixture of utilities and augmentations for all of these various technologies, and often internally mixes usage of all of these technologies to create further support.

For example, I have a String Utilities.h and String Utilities.cpp which provide a plethora of string-related free-functions and even a class or two.

And often I find that I have a pair of functions, one that works without need of MFC or its CStrings, and another sibling function that does need these things. For example:

////////////////////////////////////////////////////////////////////////
// Line Terminator Manipulation
////////////////////////////////////////////////////////////////////////

// AnsiToUnix() Convert Mac or PC style string to Unix style string (i.e. no CR/LF or CR only, but rather LF only)
// NOTE: in-place conversion!
TCHAR * AnsiToUnix(TCHAR * pszAnsi, size_t size);

template <typename T, size_t size>
T * AnsiToUnix(T (&pszBuffer)[size]) { return AnsiToUnix(pszBuffer, size); }

inline TCHAR * AnsiToUnix(Toolbox::AutoCStringBuffer & buffer) { return AnsiToUnix(buffer, buffer.size()); }

// UnixToAnsi() Converts a Unix style string to a PC style string (i.e. CR or LF alone -> CR/LF pair)
CString UnixToAnsi(const TCHAR * source);

As you can see, AnsiToUnix doesn't require a CString. Because Unix uses a single Carriage Return as a line terminator, and Windows ANSI strings use CR+LF as a line terminator, I am guaranteed that the resulting string will fit within the original buffer space. But for the reverse conversion, the string is almost guaranteed to grow, adding an extra LF for every occurrence of a CR, and hence it is desirable to use a CString (or perhaps a std::string) to provide for the automatic growth of the string.

This is just one example, and in and of itself, is not too beastly to consider converting from CString to std::string to remove the dependency upon MFC from that portion of the library. However, there are other examples where the dependency is much more subtle, and the work greater to change it. Further, the code is well tested as is. If I go and try to remove all MFC dependencies, I am likely to introduce subtle errors to the code, which would potentially compromise our product, and exacerbate the amount of time needed on this essentially not-strictly-necessary task.

The important thing I wanted to get across is that here we have a set of functions, all very related to one another (ANSI->UNIX, UNIX->ANSI), but where one side uses MFC, and the other only uses character arrays. So, if I am trying to provide a library header that is as reusable as possible, it is desirable to break out the functions which are all dependent on MFC into one header, and those which are not into another, so that it is easier to distribute said files to other projects which do not employ MFC (or whatever technology is in question: e.g. It would be desirable to have all functions which don't require Win32 headers - which are simply augmentations to C++, to have their own header, and etc.).

My question to all of you, is how do you manage these issues - Technology dependency vs. related functions all being in the same place?

How do you break down your libraries - divide things out? What goes with what?

Perhaps it is important to add my motivation: I would like to be able to publish articles and share code with others, but generally speaking, they tend to use portions of the MFC Toolbox Library, which themselves use other parts, creating a deep web of dependencies, and I don't want to burden the reader / programmer / consumer of these articles and code-projects with so much baggage!

I can certainly strip out just the parts needed for a given article or project, but that seems like a time-intensive and pointless endeavor. It would be much more sensible, to my mind, to clean up the library in such a way that I can more easily share things without dragging the entire library with me. i.e. Reorganize it once, rather than having to dig things out each and every time...


Here's another good example:

UINT GetPlatformGDILimit()
{
    return CSystemInfo::IsWin9xCore() ? 0x7FFF : 0x7FFFFFFF;
}

GetPlatformGDILimit() is a fairly generic, utilitarian free function. It really doesn't have anything to do with CSystemInfo, other than as a client. So it doesn't belong in "SystemInfo.h". And it is just a single free-function - surely nobody would try to put it in its own header? I have placed it in "Win32Misc.h", which has an assortment of such things - free functions mostly which augment the Win32 API. Yet, this seemingly innocuous function is dependent upon CSystemInfo, which itself uses CStrings and a whole slew of other library functions to make it able to do its job efficiently, or in fewer lines of code, or more robustly, or all of the above.

But if I have a demo project that refers to one or two functions in the Win32Misc.h header, then I get into the bind of needing to either extract just the individual functions that project needs (and everything that those functions depends upon, and everything those entities depend upon, etc.) -- or I have to try to include the Win32Misc.h and its .cpp - which drags even more unwanted overhead with them (just in order for the demo project to compile).

So what rule of thumb do folks use to guide yourselves as to where to draw the line - what goes with what? How to keep C++ libraries from becoming a dependency tree from hell? ;)

A: 

If you use only conformat types in your public interfaces, and keep the interfaces seperated from the implementations, this becomes a non-issue.

Keep in mind that when you introduce a function like this:

std::string ProcessData();

...and put the source code for this in a module separate from the code that will call it (for example, in a DLL), you break the seperate-interface-from-implementation edict. This is because the STL is a source code library, and every compiler that uses your library functions can and will have different implementations and different binary layouts for the utilities you use.

John Dibling
Can you clarify what you mean by "conformant types"?
Emile Cormier
@Emile: I'm using the term somewhat loosely, but what I mean is types defined in the C++ standard. Aggregates/derivitaves of C++ types would also be conformant.
John Dibling
This is a statically linked library, so there's no issues with DLLs.
Mordachai
Mordachai
A: 

In a very vague answer, KISS is the best policy. However, it seems that the code has come too far and has reached the point of no return. This is unfortunate because what you would want to do is have separate libraries that are autonomous entities, meaning they don't depend on any outside stuff. You create an MFC helper functions library and another library for other helpers or whatever. Then you decide which ones you want and when. All the dependencies are within each library and they are stand-alone.

Then it just becomes a matter of which library to include or not.

Also, using condition includes within header files works well if you want only certain things under certain scenarios. However, I'm still not entirely sure if I have interpreted the problem correctly.

Brian T Hannan
+1  A: 

Personally I'd break it down on functionality. String manipulation in one library. Integral types in another (except perhaps char put that into the string lib)

I would certainly keep platform dependant stuff away from non platform dependant stuff. Vendor specific stuff away from the non vendor specific. This might require two or even three string libraries.

Perhaps you could use the paradigm "does it require MFC?" anything that requires mfc should be split out. Then move on to "does it require windows" again do some splitting. and so forth...

Without a doubt some projects will require all libraries have to be compiled in VC++ and only run on windows, that's just the way it goes. Other projects will happily compile on linux using just a subset of the libraries and compilable with gcc.

DC

DeveloperChris