April 01, 2004
Flexible Implementations Without Using DirectivesMatthew Wilson
It is widely accepted that using directives are an unacceptable part of the public interface of library code. This is because a using directive indiscriminately makes visible all symbols within the referenced namespace from the point of directive declaration, raising the potential for symbol clash (which is what namespaces were created to circumvent).
Where debate does exist, however, is regarding the use of using directives in the implementation of library or application code. Proponents acknowledge that you shouldn't do it in your headers, but suggest that it is, if not good, at worst benign in implementation files. This epitomizes a philosophy of doing no harm to others, but not impinging on one's right to balance risks for oneself.
The desire to use using directives in this context comes from two motivations. The first is convenienceyou may call it laziness, I couldn't possibly commentsince if you use using declarations or explicitly qualify every symbol, you have a lot more typing. Consider the following block of code, which uses using declarations.
// with using declarations
using winstl::performance_counter;
using winstl::findfile_sequence;
void measure_interval(. . .)
{
performance_counter counter;
findfile_sequence sequence("*.*");
findfile_sequence::const_iterator begin = sequence.begin();
findfile_sequence::const_iterator end = sequence.end();
for(counter.start(); begin != end; ++begin)
{
. . .
}
counter.stop();
. . .
If each symbol from the winstl namespace is explicitly qualified, this takes the more verbose form.
// with explicit qualification
void measure_interval(. . .)
{
winstl::performance_counter counter;
winstl::findfile_sequence sequence("*.*");
winstl::findfile_sequence::const_iterator begin = sequence.begin();
winstl::findfile_sequence::const_iterator end = sequence.end();
. . .
The minimum amount of typing is by making use of using directives:
// with using directives
using namespace winstl;
void measure_interval(. . .)
{
performance_counter counter;
findfile_sequence sequence("*.*");
findfile_sequence::const_iterator begin = sequence.begin();
findfile_sequence::const_iterator end = sequence.end();
. . .
I have my own opinions on this motivation, but I'd be inviting a religious war to comment further than saying that it is not compelling.
The second motivationflexibilitycarries a lot more weight. A claimed advantage to the using directive form is that if and when one wishes to replace the constructs from the current namespace with those from another, it is a simple one-line change. For example, suppose we wish to port our code to UNIX, using synonymous constructs from the unixstl namespace.
// with using directives
#ifdef WIN32
using namespace winstl;
#elif defined(unix)
using namespace unixstl;
# . . .
void measure_interval(. . .)
{
. . .
With the other two approaches, there would be a lot more typing, as in:
// with using declarations #ifdef WIN32 using winstl::performance_counter; using winstl::findfile_sequence; #elif defined(unix) using unixstl::performance_counter; using unixstl::findfile_sequence; # . . .or
// with explicit qualification
#ifdef WIN32
winstl::performance_counter counter;
winstl::findfile_sequence sequence("*.*");
winstl::findfile_sequence::const_iterator begin = sequence.begin();
winstl::findfile_sequence::const_iterator end = sequence.end();
#elif defined(unix)
unixstl::performance_counter counter;
unixstl::findfile_sequence sequence("*.*");
unixstl::findfile_sequence::const_iterator begin = sequence.begin();
unixstl::findfile_sequence::const_iterator end = sequence.end();
# . . .
Naturally, very few people would choose to write code such as the latter form. More likely, they would define their own preprocessor symbol to substitute the namespace name in light of platform discrimination, as in:
#ifdef WIN32
# define THE_NS winstl
#elif defined(unix)
# define THE_NS unixstl
. . .
THE_NS::performance_counter counter;
THE_NS::findfile_sequence sequence("*.*");
THE_NS::findfile_sequence::const_iterator begin = sequence.begin();
THE_NS::findfile_sequence::const_iterator end = sequence.end();
But we know that use of the preprocessor is depreciated in C++, and is to be avoided wherever possible [4].
Hence, we can say that the explicit qualification and using declaration approaches are robust (namespace symbol visibility and use is selective, and under explicit programmer control) but inflexible, whereas the using directives approach is flexible (usually only one change is needed) but less robust. This is true as far as it goes, but using directives is a big sledgehammer to crack what is actually a very small nut.
There is a solution that is as robust as explicit qualification and using declarations, and as flexible and convenient as using directives. In a previous article [5], I described a simple but very useful application of the technique of namespace aliasing [6] for facilitating flexibility in effecting benign changes to open-source header files. Namespace aliasing can also be applied here to address our implementation flexibility problem. The discriminated namespace is aliased to an independent symbol, which may then be used for using declarations or for explicit qualification. Any necessary platform discrimination is significantly simplified and localized.
According to your own tastes you can elect to use either of two variants, with using declarations:
// with using declaration and namespace aliasing
#ifdef WIN32
namespace platform_stl = winstl;
#elif defined(unix)
namespace platform_stl = unixstl;
# . . .
using platform_stl::performance_counter;
using platform_stl::findfile_sequence;
void measure_interval(. . .)
{
performance_counter counter;
findfile_sequence sequence("*.*");
findfile_sequence::const_iterator begin = sequence.begin();
findfile_sequence::const_iterator end = sequence.end();
. . .
or with explicit qualification:
// with explicit qualification and namespace aliasing
#ifdef WIN32
namespace platform_stl = winstl;
#elif defined(unix)
namespace platform_stl = unixstl;
# . . .
void measure_interval(. . .)
{
platform_stl::performance_counter counter;
platform_stl::findfile_sequence sequence("*.*");
platform_stl::findfile_sequence::const_iterator begin = sequence.begin();
platform_stl::findfile_sequence::const_iterator end = sequence.end();
. . .
The technique is very simple, but it is surprising how often it is overlooked.
Notes and References[1] More Exceptional C++, Herb Sutter, Addison-Wesley 2002. [2] "Uncaught Exceptions", Bobby Schmidt, C/C++ User's Journal, September 2001. [3] "Uncaught Exceptions", Bobby Schmidt, C/C++ User's Journal, January 2002. [4] It should be noted that discriminating against existingin so far as they are defined by your compiler and/or platformsymbols is less fraught than creating your own. Of course it is possible, though less manageable, to eliminate all the preprocessor logic entirely, by working with include paths. [5] "Open-Source Flexibility via Namespace Aliasing," Matthew Wilson, C/C++ User's Journal, July 2003. [6] The C++ Programming Language, 3rd Edition, Bjarne Stroustrup, Addison-Wesley, 1997.
About the AuthorMatthew Wilson is a software development consultant for Synesis Software, and creator of the STLSoft libraries. He is author of the forthcoming book Imperfect C++ (to be published this fall by Addison-Wesley, 2004), and is currently working on his next two books, one of which is not about C++. Matthew can be contacted via http://stlsoft.org/.
| |||||||||