Solution

I l @ ve RuBoard

graphics/bulb.gif

Recap: Nested and Local Classes

C++ provides many useful tools for information hiding and dependency management. As we recap nested classes and local classes, don't fixate too much on the syntax and semantics. Rather, focus on how these features can be used to write solid, maintainable code and express good object designs ”designs that prefer weak coupling and strong cohesion.

1. What is a nested class? Why can it be useful?

A nested class is a class enclosed within the scope of another class. For example:

 // Example 33-1: Nested class // class OuterClass { /*...public, protected, or private...*/:   class NestedClass   {     // ...   };   // ...   }; 

Nested classes are useful for organizing code and controlling access and dependencies. Nested classes obey access rules just as other parts of a class do. So, in Example 33-1, if NestedClass is public , then any outside code can name it as Outer Class::NestedClass . Often, nested classes contain private implementation details, and are therefore made private . In Example 33-1, if NestedClass is private , then only OuterClass 's members and friends can use NestedClass .

Note that you can't get the same effect with namespaces alone, because namespaces merely group names into distinct areas. Namespaces do not by themselves provide access control, whereas classes do. So, if you want to control access rights to a class, one tool at your disposal is to nest it within another class.

2. What is a local class? Why can it be useful?

A local class is a class defined within the scope of a function ”any function, whether a member function or a free function. For example:

 // Example 33-2: Local class // int f() {   class LocalClass   {     // ...   };   // ... }; 

Like nested classes, local classes can be a useful tool for managing code dependencies. In Example 33-2, only the code within f() itself knows about LocalClass and is able to use it. This can be useful when LocalClass is, say, an internal implementation detail of f() that should never be advertised to other code.

You can use a local class in most places where you can use a nonlocal class, but there is a major restriction to keep in mind. A local or unnamed class cannot be used as a template parameter. From the C++ standard [C++98] section 14.3.1/2:

A local type, a type with no linkage, an unnamed type or a type compounded from any of these types shall not be used as a template-argument for a template type-parameter . [Example:

  template<class T>   class X { /*   ...   */ };   void f()   {   struct S { /*   ...   */ };   X<S> x3;  // error: local type used as   //  template-argument   X<S*> x4; // error: pointer to local type   //  used as template-argument   }  

--end example]

Both nested classes and local classes are among C++'s many useful tools for information hiding and dependency management.

Nested Functions: Overview

Some languages (but not C++) allow nested functions, which have similarities to nested classes. A nested function is defined inside another function, the "enclosing function," such that:

  • The nested function has access to the enclosing function's variables ; and

  • The nested function is local to the enclosing function ”that is, it can't be called from elsewhere unless the enclosing function gives you a pointer to the nested function.

Just as nested classes can be useful because they help control the visibility of a class, nested functions can be useful because they help control the visibility of a function.

C++ does not have nested functions. But can we get the same effect? This brings us to the following key question.

3. C++ does not support nested functions. That is, we cannot write something like

  // Example 33-3   //   int f(int i)   {   int j = i*2;   int g(int k)  // not valid C++   {   return j+k;   }   j += 4;   return g(3);   }  

Demonstrate how it is possible to achieve the same effect in standard C++, and show how to generalize the solution.

Yes, it is possible, with only a little code reorganization and a minor limitation. The basic idea is to turn a function into a function object. This discussion, not coincidentally, will also serve to nicely illustrate some of the power of function objects.

Attempts at Simulating Nested Functions in C++

To solve a problem like Question #3, most people start out by trying something like the following:

 // Example 33-3(a): Nave "local function object" // approach (doesn't work) // int f(int i) {   int j = i*2;   class g_   {   public:     int operator()(int k)     {       return j+k;   // error: j isn't accessible     }   } g;   j += 4;   return g(3); } 

In Example 33-3(a), the idea is to wrap the function in a local class and call the function through a function object.

It's a nice idea, but it doesn't work for a simple reason: The local class object doesn't have access to the enclosing function's variables.

"Well," one might say, "why don't we just give the local class pointers or references to all the function's variables?" Indeed, that's usually the next attempt.

 // Example 33-3(b): Nave "local function object plus // references to variables" approach (complex, // fragile) // int f(int i) {   int j = i*2;   class g_   {   public:     g_(int& j) : j_(j) { }     int operator()(int k)     {       return j_+k;  // access j via a reference     }   private:     int& j_;   } g(j);   j += 4;   return g(3); } 

All right, I have to admit that this "works" ”but only barely . This solution is fragile, difficult to extend, and can rightly be considered a hack. For example, consider that just to add a new variable requires four changes:

  1. Add the variable;

  2. Add a corresponding private reference to g_ ;

  3. Add a corresponding constructor parameter to g_ ; and

  4. Add a corresponding initialization to g_::g_() .

That's not very maintainable. It also isn't easily extended to multiple local functions. Couldn't we do better?

A Somewhat Improved Solution

We can do better by moving the variables themselves into the local class.

 // Example 33-3(c): A better solution // int f(int i) {   class g_   {   public:     int j;     int operator()(int k)     {       return j+k;     }   } g;   g.j = i*2;   g.j += 4;   return g(3); } 

This is a solid step forward. Now g_ doesn't need pointer or reference data members to point at external data; it doesn't need a constructor; and everything is much more natural all around. Note also that the technique is now extensible to any number of local functions, so let's go a little crazy and add some more local functions called x() , y() , and z() , and while we're at it, why not rename g_ itself to be more indicative of what the local class is actually doing?

 // Example 33-3(d): Nearly there! // int f(int i) {   // Define a local class that wraps all   // local data and functions.   //   class Local_   {   public:     int j;     // All local functions go here:     //     int g(int k)     {       return j+k;     }     void x() { /* ... */ }     void y() { /* ... */ }     void z() { /* ... */ }   } local;   local.j = i*2;   local.j += 4;   local.x();   local.y();   local.z();   return local.g(3); } 

This still has the problem that when you need to initialize j using something other than default initialization, you have to add a clumsy constructor to the local class just to pass through the initialization value. The original question tried to initialize j to the value of i*2 . Here, we've had to create j and then assign the value, which isn't exactly the same thing and could be difficult for more-complex types.

A Complete Solution

If you don't need f itself to be a bona fide function (for example, if you don't take pointers to it), you can turn the whole thing into a function object and elegantly support non-default initialization into the bargain.

 // Example 33-3(e): A complete and nicely // extensible solution // class f {   int  retval; // f's "return value"   int  j;   int  g(int k) { return j + k; };   void x() { /* ... */ }   void y() { /* ... */ }   void z() { /* ... */ } public:   f(int i)   // original function, now a constructor     : j(i*2)   {     j += 4;     x();     y();     z();     retval = g(3);   }   operator int() const // returning the result   {     return retval;   } }; 

The code is shown inline for brevity, but all the private members could also be hidden behind a Pimpl, giving the same full separation of interface from implementation we had with the original simple function.

Note that this approach is easily extensible to member functions. For example, suppose that f() is a member function instead of a free function, and we would like to write a nested function g() inside f() as follows :

 // Example 33-4: This isn't legal C++, but it // illustrates what we want: a local function // inside a member function // class C {   int data_; public:   int f(int i)   {     // a hypothetical nested function     int g(int i) { return data_ + i; }     return g(data_ + i*2);   } }; 

We can express this by turning f() into a class, as demonstrated in Example 33-3(e), except whereas in Example 33-3(e) the class was at global scope, it is now a nested class and accessed via a passthrough helper function.

 // Example 33-4(a): The complete and nicely // extensible solution, now applied to a // member function // class C {   int data_;   friend class C_f; public:   int f(int i); }; class C_f {   C*  self;   int retval;   int g(int i) { return self->data_ + i; } public:   C_f(C* c, int i) : self(c)   {     retval = g(self->data_ + i*2);   }   operator int() const { return retval; } }; int C::f(int i) { return C_f(this, i); } 

Summary

This approach illustrated above in Examples 33-3(e) and 33-4(a) simulates most of the features of local functions and is easily extensible to any number of local functions. Its main weakness is that it requires that all variables be defined at the start of the "function" and doesn't (easily) allow us to declare variables closer to point of first use. It's also more tedious to write than a local function would be, and it is clearly a workaround for a language limitation. But, in the end, it's not bad, and it demonstrates some of the power inherent in function objects over plain functions.

The purpose of this Item wasn't just to learn about how nice it might be to have local functions in C++. The purpose was, as always, to set a specific design problem, explore alternative methods for solving it, and weigh the solutions to pick the best-engineered option. Along the way, we also got to experiment with various C++ features and see through use what makes function objects useful.

When designing your programs, strive for simplicity and elegance wherever possible. Some of the intermediate above solutions would arguably "work" but should never be allowed to see the light of day in production code. They are complex, difficult to understand, and therefore difficult and expensive to maintain.

Guideline

graphics/guideline.gif

Prefer clarity. Avoid complex designs. Eschew obfuscation.


Simpler designs are easier to code and test. Avoid complex solutions, which are almost certain to be fragile and harder to understand and maintain. While being careful to avoid the trap of overengineering, recognize that even if coming up with a judicious simple design takes a little extra time up front, the long- term benefits usually make that time well spent. Putting in a little time now often means saving more time later. And most people would agree that the "more time later" is better spent with the family at the cottage than slaving away over a hot keyboard, trying to find those last few bugs in a complicated rat's nest of code.

I l @ ve RuBoard


More Exceptional C++
More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions
ISBN: 020170434X
EAN: 2147483647
Year: 2001
Pages: 118
Authors: Herb Sutter

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net