Sometimes developers will create a typedef/class/struct and use it for more than one conceptual purpose. In the case of the legacy C code I was porting to C++/CLI, they had a typedef of UU8 to use as shorthand for an unsigned char. They used this UU8 type anywhere they needed a byte, a char, or char array as a string. I then tried to compile this program in VS 2008 as a native C++ app(I took things one step at a time, not yet enabling the CLR at this point). I got almost a hundred errors regarding UU8* failing to cast to a char*. This is to be expected, because by default char is signed. There are compiler options that change this default, but they were not present in the VS 6.0 projects, nor did the command line options for VS 2008’s compiler effect this either. I resorted to changing the typedef to signed char. The application compiled successfully and run, but I noticed that some of the graphics in the C based UI were slightly corrupt, and a couple of other critical features of the application were not working. I assumed these issues were caused by compiling the C code as C++. After a significant amount of investigation, I realized that the UU8 type was being used for some low level calculations. Because it was now signed, the calculations were failing.
What I really needed, was to split the usage of the type into two categories. I created a typedef for the signed char, and reverted the old UU8 typedef to unsigned char. I then had to go through the code and systematically replace any usage of UU8 with the new signed char type, if it was being used as a char or string. This meant digging through code I was not familiar with, and analyzing the semantics of the usage of the UU8 type everywhere that it appeared, before deciding whether it should actually be using signed char. In some cases I got carried away and made the replacement where I shouldn’t have. Some places would have a variable or parameter called something like ClassDesc or ItemDesc, and the Desc suffix would lead me to believe it was a string, when actually it was some data intended to be used as UU8.
This is the problem of trying to take an existing declaration of a typedef, class, struct, or other type, and reuse it where it was not intended. It would have been one extra line of code for the developer to declare two different typedefs, instead of sharing the typedef across two totally different types. I don’t knock the original developer though, as it is clear he was leaning towards productivity, rather than maintainability/readability, which is perfectly valid given the type of project.
Where this really affects readability is in a case(which occurred quite often in the above project) where a function takes multiple parameters, and some of those parameters are decalred as the same type, but expect totally different conceptual types of data.
For example something like this is really ambiguous and not self documenting:
FindItemName(UU8 * SearchName, UU8 * SearchOption, UU8 * ToBeSearched);
SearchOptions is a pointer for passing flags that affect the behavior of the search function. It is essentially a piece of binary data that will more than likely be operated on with lots of bitwise operations. SearchName and ToBeSearched are char arrays to be operated on as strings. This makes calling this function very confusing, and when you run into cases where the names of the parameters imply the opposite (such as the “Desc” suffix example I gave above), then it becomes two fold as confusing, because you then have to consider 2 to the second power possible meanings of each variable.
This is not a problem isolated to C or C++. C# as well can experience this problem, and developers are even tempted to create this problem. Interoperability using classes and interfaces is strongly encouraged. Some developers go as far as to create an interface for every class they create. Sometimes they try to force classes into having common interfaces for the sake of reusing code, even when they don’t yet have, or anticipate, any concrete implementations that would take advantage of the commonality. You run into cases where very different classes share an interface, but the behavior of the interface is so vastly different between each class’s implementation that you’d never ever want to use them interchangeably.
One must not be tempted to force a class into an interface where it does not conceptually or semantically fit, just as one should not try to fit the square block into the round hole. The .NET Framework Design Guidelines book provides some tips along the lines of having an implementation of at least one class for each interface, to exercise that interface and make sure the interface essentially makes sense. I would go further to say that additional classes implementing that interface should pass a usage test. Consider existing code that interacts with the first class, via its interface. Now consider what would happen if the new class that implements this interface was used in the existing code. It will, of course, compile because the code accesses the class via the common interface, but the real question is whether its usage makes conceptual sense, and does not result in logic errors.
If we look at an interface like IDisposable, what we see is that it does not just defined syntactically, but is also defined conceptually. It is expected that classes implementing methods of IDisposable, such as Dispose, implement them with a certain behavior. Should the class implement the methods only so much as to allow for successful compilation by meeting the syntactic requirements, but do not meet the behavioral/semantic requirements, then users of the class will be very surprised by the problems they encounter. If the class has unmanaged resources, then an improper implementation of the Dispose method will result in resource/memory management issues for users of that class. Maybe, as the developer of the reusable class, I decided to have a Free() method that releases a file handle, and did not implement this functionality in the Dispose method. This would be stupid, as users might instantiate my class using a the using keyword, and be surprised that the file handle is not deterministically released. I failed to follow the behavioral definition of implementing IDisposable, and thus my use of the interface does more harm than good by luring my users into a false belief that calling Dispose deterministically releases unmanaged resources.