- Namespaces
- A Brief Historical Background
- Properties of Namespaces
- Interaction with Other Language Features
- Conclusions
Properties of Namespaces
Namespaces are more than just name containers. They were designed to allow fast and simple migration of legacy code without incurring any overhead. Namespaces have several properties that facilitate their usage: a fully qualified name, a using-declaration and a using-directive.
A Fully Qualified Name
A namespace is a scope in which declarations and definitions are grouped together. In order to refer to any of these from another scope, a fully qualified name is required. A fully qualified name has the form: namespace::classname::identifier. Since both namespaces and classes can be nested, the resulting name can be pretty long; however, this name ensures unique identification:
unsigned int maxlen=std::string::npos;
Still, repeating the fully qualified name is both tedious and less readable. Instead, you can use a using declaration or a using directive.
A using-Declaration and a using-Directive
A using-declaration consists of the keyword using followed by a namespace::member. It instructs the compiler to locate every occurrence of a certain namespace member (type, operator, function, constant, and so on) in the specified namespace, as if the fully-qualified name were used:
{ /* the following is a using declaration; every occurrence of vector is looked up in std */ using std::vector; vectorvi; }
A using-directive, on the other hand, renders all the names of a certain namespace accessible in the directive's scope. For example:
#include// belongs to namespace std #include // also in namespace std int main() { using namespace std; // a using directive
/* alland declarations can now
be accessed without fully qualified names */ vectorvi; vi.push_back(10); cout< Let's look back at our string class example (code is repeated here for convenience).
//file excelSoftCompany.h namespace excelSoftCompany { class string {/*..*/}; class vector {/*..*/}; }
You can access your own string class and the standard string class
in the same program now:#include// std::string #include "excelSoftCompany.h" int main() { using namespace excelSoftCompany; string s; // referring to class excelSoftCompany::string std::string standardstr; // create an ANSI string } Namespaces Are Extensible
You can define a namespace in several source files:
//file proj_const.h
namespace MyProj { enum NetProtocols { TCP_IP, HTTP, UDP }; } //file proj_classes.h
namespace MyProj // extending MyProj namespace { class RealTimeEncoder{ public: NetProtocols detect(); }; class NetworkLink {/*..*/}; class UserInterface {/*..*/}; }In this example, I defined the namespace MyProj in two separate files, each of which contains a portion of that namespace. The complete MyProj namespace can be extracted from both files like this:
//file app.cpp #include "proj_const.h" #include "proj_classes.h" int main() { using namespace MyProj; RealTimeEncoder encoder; NetProtocols protocol; protocol = encoder.detect(); }Koenig Lookup
Andrew Koenig devised an algorithm for resolving namespace members. This algorithm, also called argument dependent lookup, is used in all standard-conforming compilers to handle cases like the following:
namespace MYNS { class C {}; void func(C); } MYNS::C c; // global object of type MYNS::C int main() { func(c); // OK, MYNS::f called }Neither a using-declaration nor a using-directive appears in the program. And yet, the compiler did the right thing: It correctly identified the unqualified name func as the function declared in namespace MYNS by applying Koenig lookup. How does it work? Koenig lookup instructs the compiler to look at not just the usual places (such as the local scope), but also the namespace that contains the argument's type. Thus, in the following source line, the compiler detects that the object c, which is the argument of the function func, belongs to namespace MYNS. Consequently, it looks at namespace MYNS to locate the declaration of func, "guessing" the programmer's intent.
func(c); // OK, MYNS::f calledWithout Koenig lookup, namespaces would impose an unacceptable burden on the programmer, who would have to repeatedly specify the fully qualified names, or instead use numerous using-declarations. To push the argument in favor of Koenig lookup even further, consider the following example:
#includeusing std::cout; int main() { cout<<"hi"; //OK, << brought into scope by Koenig lookup } The using-declaration injects std::cout into the scope of main(), thereby enabling the programmer to use the non-qualified name cout. However, the overloaded << operator, as you may recall, is not a member of std::cout. Rather, << is declared in namespace std as an external function that takes a std::ostream object as its argument. Without Koenig lookup, the programmer would have to write something like this:
std::operator<<(cout, "hi");Alternatively, the programmer would have to place a using namespace std; directive in the program. None of these options are desirable because they clutter up code and might become a source of confusion and errors. (using-directives are the least favorable form of rendering names that are visible in the current scope because they indiscriminately make all the members of a namespace visible). Fortunately, Koenig lookup "does the right thing" and elegantly saves us from this tedium.
Koenig lookup is applied automatically. It does not require any special directives or configuration switches to be activated, nor can it be turned off. This fact has to be borne in mind, because it can have surprising results under some circumstances as in this example:
namespace NS1 { class B{}; void f(B); }; void f(NS1::B); // a global function int main() { NS1::B b; f(b); // ambiguous; NS1::f() or f(NS1::B)? }Compilers that are not standard-conforming will not complain about the ambiguous call to function f — they will simply choose one of its versions. The version they choose, however, might not be the one that the programmer had in mind. Furthermore, the problem can arise at a later stage of the development, when additional versions of f are added to the project and are confusing to the compiler's lookup algorithm. This ambiguity is not confined to global names. It might also appear when two namespaces relate to one another — for example, if a namespace declares a class that is used as a parameter of a member function that is declared in a different namespace. In a well-designed project, this problem would rarely arise. Good object-oriented design is based on maximal de-coupling. Still, you should be aware of such possible side effects.
Namespace resolution, including Koenig lookup, is resolved at compile-time. The underlying implementation of namespaces is done by means of name-mangling, which is an algorithm the compiler uses to create unique names for functions, classes, templates, and variables. Therefore, namespaces do not incur any runtime or memory overhead.