B. Stroustrup - The C++ Programming Language (794319), страница 88
Текст из файла (страница 88)
For example:namespace N {struct S { int i };void f(S);void g(S);void h(int);}Section 14.2.4Argument-Dependent Lookup397struct Base {void f(N::S);};struct D : Base {void mf();void g(N::S x){f(x);// call Base::f()mf(x);// call D::mf()h(1);// error: no h(int) available}};In the standard, the rules for argument-dependent lookup are phrased in terms of associated namespaces (§iso.3.4.2).
Basically:• If an argument is a class member, the associated namespaces are the class itself (includingits base classes) and the class’s enclosing namespaces.• If an argument is a member of a namespace, the associated namespaces are the enclosingnamespaces.• If an argument is a built-in type, there are no associated namespaces.Argument-dependent lookup can save a lot of tedious and distracting typing, but occasionally it cangive surprising results. For example, the search for a declaration of a function f() does not have apreference for functions in a namespace in which f() is called (the way it does for functions in aclass in which f() is called):namespace N {template<class T>void f(T, int); // N::f()class X { };}namespace N2 {N::X x;void f(N::X, unsigned);void g(){f(x,1);}// calls N::f(X,int)}It may seem obvious to choose N2::f(), but that is not done.
Overload resolution is applied and thebest match is found: N::f() is the best match for f(x,1) because 1 is an int rather than an unsigned.Conversely, examples have been seen where a function in the caller’s namespace is chosen but theprogrammer expected a better function from a known namespace to be used (e.g., a standard-libraryfunction from std). This can be most confusing. See also §26.3.6.398NamespacesChapter 1414.2.5 Namespaces Are OpenA namespace is open; that is, you can add names to it from several separate namespace declarations.
For example:namespace A {int f();// now A has member f()}namespace A {int g();// now A has two members, f() and g()}That way, the members of a namespace need not be placed contiguously in a single file. This canbe important when converting older programs to use namespaces. For example, consider a headerfile written without the use of namespaces:// my header:void mf();void yf();int mg();// ...// my function// your function// my functionHere, we have (unwisely) just added the declarations needed without concerns of modularity.
Thiscan be rewritten without reordering the declarations:// my header:namespace Mine {void mf();// my function// ...}void yf();// your function (not yet put into a namespace)namespace Mine {int mg();// my function// ...}When writing new code, I prefer to use many smaller namespaces (see §14.4) rather than puttingreally major pieces of code into a single namespace. However, that is often impractical when converting major pieces of software to use namespaces.Another reason to define the members of a namespace in several separate namespace declarations is that sometimes we want to distinguish parts of a namespace used as an interface from partsused to support easy implementation; §14.3 provides an example.A namespace alias (§14.4.2) cannot be used to re-open a namespace.Section 14.3Modularization and Interfaces39914.3 Modularization and InterfacesAny realistic program consists of a number of separate parts.
For example, even the simple ‘‘Hello,world!’’ program involves at least two parts: the user code requests Hello, world! to be printed, andthe I/O system does the printing.Consider the desk calculator example from §10.2. It can be viewed as composed of five parts:[1] The parser, doing syntax analysis: expr(), term(), and prim()[2] The lexer, composing tokens out of characters: Kind, Token, Token_stream, and ts[3] The symbol table, holding (string,value) pairs: table[4] The driver: main() and calculate()[5] The error handler: error() and number_of_errorsThis can be represented graphically:drivererror handlerparserlexersymbol tablewhere an arrow means ‘‘using.’’ To simplify the picture, I have not represented the fact that everypart relies on error handling.
In fact, the calculator was conceived as three parts, with the driverand error handler added for completeness.When one module uses another, it doesn’t need to know everything about the module used. Ideally, most of the details of a module are unknown to its users. Consequently, we make a distinctionbetween a module and its interface. For example, the parser directly relies on the lexer’s interface(only), rather than on the complete lexer. The lexer simply implements the services advertised inits interface. This can be presented graphically like this:drivererror handlerparser interfaceparser implementationlexer interfacelexer implementationsymbol table interfacesymbol table implementationA dashed line means ‘‘implements.’’ I consider this to be the real structure of the program, and ourjob as programmers is to represent this faithfully in code.
That done, the code will be simple, efficient, comprehensible, maintainable, etc., because it will directly reflect our fundamental design.400NamespacesChapter 14The following subsections show how the logical structure of the desk calculator program can bemade clear, and §15.3 shows how the program source text can be physically organized to takeadvantage of it. The calculator is a tiny program, so in ‘‘real life’’ I wouldn’t bother using namespaces and separate compilation (§2.4.1, §15.1) to the extent done here.
Making the structure ofthe calculator explicit is simply an illustration of techniques useful for larger programs withoutdrowning in code. In real programs, each ‘‘module’’ represented by a separate namespace willoften have hundreds of functions, classes, templates, etc.Error handling permeates the structure of a program. When breaking up a program into modules or (conversely) when composing a program out of modules, we must take care to minimizedependencies between modules caused by error handling. C++ provides exceptions to decouple thedetection and reporting of errors from the handling of errors (§2.4.3.1, Chapter 13).There are many more notions of modularity than the ones discussed in this chapter and the next.For example, we might use concurrently executing and communicating tasks (§5.3, Chapter 41) orprocesses to represent important aspects of modularity.
Similarly, the use of separate address spaces and the communication of information between address spaces are important topics not discussed here. I consider these notions of modularity largely independent and orthogonal. Interestingly, in each case, separating a system into modules is easy. The hard problem is to provide safe,convenient, and efficient communication across module boundaries.14.3.1 Namespaces as ModulesA namespace is a mechanism for expressing logical grouping. That is, if some declarations logically belong together according to some criteria, they can be put in a common namespace toexpress that fact. So we can use namespaces to express the logical structure of our calculator. Forexample, the declarations of the parser from the desk calculator (§10.2.1) may be placed in a namespace Parser:namespace Parser {double expr(bool);double prim(bool get) { /* ... */ }double term(bool get) { /* ...
*/ }double expr(bool get) { /* ... */ }}The function expr() must be declared first and then later defined to break the dependency loopdescribed in §10.2.1.The input part of the desk calculator could also be placed in its own namespace:namespace Lexer {enum class Kind : char { /* ...
*/ };class Token { /* ... */ };class Token_stream { /* ... */ };Token_stream ts;}The symbol table is extremely simple:Section 14.3.1Namespaces as Modules401namespace Table {map<string,double> table;}The driver cannot be completely put into a namespace because the language rules require main() tobe a global function:namespace Driver {void calculate() { /* ... */ }}int main() { /* ...
*/ }The error handler is also trivial:namespace Error {int no_of_errors;double error(const string& s) { /* ... */ }}This use of namespaces makes explicit what the lexer and the parser provide to a user. Had Iincluded the source code for the functions, this structure would have been obscured. If functionbodies are included in the declaration of a realistically sized namespace, you typically have to wadethrough screenfuls of information to find what services are offered, that is, to find the interface.An alternative to relying on separately specified interfaces is to provide a tool that extracts aninterface from a module that includes implementation details. I don’t consider that a good solution.Specifying interfaces is a fundamental design activity, a module can provide different interfaces todifferent users, and often an interface is designed long before the implementation details are madeconcrete.Here is a version of the Parser with the interface separated from the implementation:namespace Parser {double prim(bool);double term(bool);double expr(bool);}double Parser::prim(bool get) { /* ...
*/ }double Parser::term(bool get) { /* ... */ }double Parser::expr(bool get) { /* ... */ }Note that as a result of separating the implementation from the interface, each function now hasexactly one declaration and one definition. Users will see only the interface containing declarations. The implementation – in this case, the function bodies – will be placed ‘‘somewhere else’’where a user need not look.Ideally, every entity in a program belongs to some recognizable logical unit (‘‘module’’).Therefore, every declaration in a nontrivial program should ideally be in some namespace named toindicate its logical role in the program. The exception is main(), which must be global in order forthe compiler to recognize it as special (§2.2.1, §15.4).402NamespacesChapter 1414.3.2 ImplementationsWhat will the code look like once it has been modularized? That depends on how we decide toaccess code in other namespaces.