|
Migrating to NamespacesThis article appeared in Dr. Dobb's Journal, 25(10), October 2000.
Although C++ changed greatly during standardization, few of the changes the committee made are likely to break existing code. There is one change, however, that will cause nearly all earlier C++ code to fail to compile, to wit: the addition of namespaces, specifically the fact that all of the C++ standard library now lives in namespace std. If you are a C++ programmer and this hasn't troubled you yet, then it probably will soon, because we're now in the situation where the newest releases of most compilers have conformed to this change. As you start upgrading to the latest release of your compiler, your code will be forced to adapt. For example, code that used to work:
now requires that you either specifically say which names are in std:
or write "using declarations" to bring the necessary std names into scope:
or write a "using directive" to simply drag all the std names into scope wholesale:
or some combination of the above. So, what's the right way to use namespaces in the long run? And what's the best way to move a large body of existing code to the new rules quickly today, deferring unnecessary (for now) migration work to the future without increasing the overall migration workload later? This article answers these questions. Guidelines For a Good Long-Term SolutionFirst, what's the right way to use namespaces in the long run? It's important to decide this first, because knowing the long-term answer will help us decide what to aim for when we craft our best short-term migration strategy. In short, a good long-term solution for namespace usage should follow at least these rules: Namespace Rule #1: Avoid using directives entirely, especially in header files. The reason for Rule #1 is that using directives cause wanton namespace pollution by bringing in potentially huge numbers of names, many (usually the vast majority) of which are unnecessary. The presence of the unnecessary names greatly increases the possibility of unintended name conflicts--not just in the header itself, but in every module that #includes the header. I find it helpful to think of a using directive as a marauding army of crazed barbarians that sows indiscriminate destruction wherever it passes--something that by its mere presence can cause "unintended conflicts," even when you think you're allied with it. Namespace Rule #2: Never write namespace using declarations in header files. Note that Rule #2 goes much further than most popular advice. Most authors recommend that using declarations never appear at file scope in shared header files. That's good advice, as far as it goes, because at file scope a using declaration causes the same kind of namespace pollution as a using directive, only less of it. Unfortunately, in my opinion the above "popular advice" doesn't go far enough. Here is why you should never write using declarations in header files at all, not even in a namespace scope: The meaning of the using declaration may change depending on what other headers happen to be #included before it in any given module. This kind of unpredictability is bad, and should never be permitted. I'll illustrate this problem later in Example 2(c). Namespace Rule #3: Use C headers with the new style "#include <cheader>" For backward compatibility with C, C++ still supports all of the standard C header names (e.g., stdio.h), and when you #include those original versions the associated C library functions are visible in the global namespace as before--but in the same breath C++ also says that the old header names are deprecated, which puts the world on notice that they may be removed in a future version of the C++ standard. Thus Standard C++ strongly encourages programmers to prefer using the new versions of the C headers that start with "c" and drop the ".h" extension (e.g., cstdio); when you #include the C headers using the new names, you get the same C library functions, but now they live in namespace std. Long-Term Approaches: A Motivating ExampleTo illustrate the above rules, consider the following example module and two good long-term approaches for migrating the module to namespaces:
How can we best migrate the above code to a compiler that respects namespaces and a library that puts the standard names in namespace std? Before reading on, please stop for a moment and think about what alternatives you might consider. Which ones are good? Which ones aren't? Why? Have you made a decision? All right, then. There are several ways you might approach this, and I'll describe two common strategies--only one of which is good form. A Good Long-Term SolutionA good long-term solution is to explicitly qualify every standard name wherever it is mentioned in a header (.h) file, and to write just the using declarations that are needed inside each source (.cpp) file for convenience, since those names are likely to be widely used in the source file:
Note that the using declarations inside x.cpp had better come after all #include directives, otherwise they may introduce name conflicts into other header files depending on the order in which they're #included. An alternative good long-term solution would be to do the above but eschew the using declarations entirely and explicitly qualify every single standard name, even inside the .cpp file. I don't recommend doing this, because it's a lot of extra work that doesn't deliver any real advantage in a typical situation. A Not-So-Good Long-Term SolutionIn contrast, one often-proposed solution is actually dangerous. This bad long-term 'solution' proposes to put all of the project's code into its own namespace (which is not objectionable in itself, but isn't as useful as one might think) and write the using declarations or directives in the headers (which unintentionally opens a gaping door for potential problems). The reason some people have proposed this method is that it requires less typing than some other namespace-migration methods:
Note the highlighted error: The reason, as stated, is that the meaning of a using declaration in a header can change--even when the using declaration is inside a namespace, and not at file scope--depending on what else a client module may happen to #include before it. Clearly, it's always bad form to write any kind of code that can silently change meaning depending on the order in which headers are #included. An Effective Short-Term SolutionThe reason why I've spent time explaining the desirable long-term solution, and discrediting some bad approaches, is because knowing what we want to eventually achieve will help us to pick a simpler short-term solution that won't compromise our long-term solution. So now we're ready to tackle the short-term question: What is the most effective way to migrate your existing code base to deal with namespace std? Speaking pragmatically, the "upgrade to the new compiler version" task is just one of a list of things to be done in our product's release cycle. Upgrading the compiler and migrating to namespaces probably isn't the most urgent item on the task list and you have lots of other things to do and product features to add; more likely than not you're under project deadline pressure to boot, so you should prefer the quickest namespace migration approach that gets the job done without compromising future safety and usability. How can you best defer unnecessary (for now) migration work to the future without increasing the overall migration workload later? First, consider migrating the header files: Migration Step #1: In every header file, add "std::" qualifiers wherever needed. "Is adding std:: everywhere really necessary?" you might ask. "Couldn't you just add using declarations or directives in the headers?" Well, adding using declarations or directives would indeed be the least work, but it's work that would have to be undone when you implement the correct long-term solution; Namespace Rules #1 and #2 presented earlier in this article already reject such an approach, and we've already seen compelling reasons not to go down that road. Given that we mustn't add using declarations or directives in headers, then, our only alternative for headers is to add "std::" in the right places. Migration Step #2: Create a new header file called myproject_last.h that contains the directive using namespace std;. In every implementation file, #include myproject_last.h after all other #includes. Things are a little better with implementation files: We can get away with simply writing a using directive in each implementation file, as long as the using directive appears after all #include statements. Writing a using directive avoids/defers the tedious work of figuring out the (possibly lengthy) correct set of using declarations that will eventually go into each implementation file; yes, that's right, as a temporary expedient you are deliberately choosing to invite an army of crazed barbarians to dinner. But never fear: as long as we're careful to write the using directive after all #includes, this doesn't compromise--or change the meaning of-- any other code in any other modules; the barbarians may be at our table for the evening, but we're chaining them to their seats. Note that the using directive is best put into a separate header which is then #included in each implementation file after all other headers, both for safety (i.e., to ensure you are sending only a single, well-controlled invitation to the army of crazed barbarians), and for another good reason that will become clearer in a moment when we consider what's left to migrate to the full long-term solution. Finally, what about the new <cheader> header style? Fortunately, that's optional, and so it doesn't need to be done during the initial migration pass, which also defers all of the associated namespace issues. Here's the result of applying our two-step migration strategy to Example 2(a):
This does not compromise the long-term solution in that it doesn't do anything that will need to be "undone" for the long-term solution. At the same time, it's simpler and requires fewer code changes than the full long-term solution would. In fact, this approach represents the minimum amount of work we can get away with that will make our code work on a namespace-aware compiler but that won't make us have to go back and undo any of the work later. Migrating To the Long-Term SolutionFinally, at some happy point in the future when you are momentarily free of pressing project deadlines, you can perform a simple migration to the full long-term solution illustrated in Example 2(b). Simply follow these steps: 1. In each header or implementation file: Change lines that #include C headers to the new <cheader> style; e.g., change "#include <stdio.h>" to "#include <cstdio>". 2. In myproject_last.h: Comment out the using directive. 3. Rebuild your project. See what breaks the compile. In each implementation file, add the correct using declarations (after all #includes). If you follow this advice, then even with looming project deadlines you'll be able to quickly--and effectively--migrate to a namespace-aware compiler and library, all without compromising your long-term solution. |
Copyright © 2009 Herb Sutter |