Toward a Less Object-Oriented View of C++

Harris makes the argument that the modular nature of C++ makes it both a weak object language and a strong general-purpose language. He adds that C++ is an object-oriented language a C programmer can appreciate because it is oriented first toward execution performance, then toward flexibility.


January 01, 1992
URL:http://www.drdobbs.com/cpp/toward-a-less-object-oriented-view-of-c/184408914

SP92: TOWARD A LESS OBJECT-ORIENTED VIEW OF C++

Hank is a technical specialist at SunPro, a division of Sun Microsystems, and can be contacted at [email protected].


The last couple of years have seen a growing wave of enthusiasm for object-oriented approaches to requirements analysis, application design, and programming. This same period has been marked by the increasing popularity of the C++ language and its acceptance as a logical successor to C. Since C++ was designed to support object-oriented development, it seems only natural to see a strong link between C++ and OOP. Developers interested in an object-oriented language may look no further than C++, while programmers who move to C++ will of course adopt an object-oriented style of programming.

Such a view is seriously mistaken. Anyone who associates C++ with OOP and OOP with C++ misses two very important points: first, that C++ is not the best language for object-oriented programming by a wide variety of measures, and second,that it is possible to get tremendous advantage from C++ without using object techniques.

OOP Languages: Same Approach to Different Goals

C++ is not a great object-oriented language, as any devotee of Smalltalk or Lisp will be glad to tell you. These other languages are easier both to learn and to use than C++. They make it possible to write better structured, more comprehensible, and more maintainable programs. Class reuse between applications, still a rarity in C++, is the rule in these languages.

Actually, the difference between C++ and these other languages is not (or at least not only) one of quality. The larger difference is one of intent. Although C++'s facilities are based on the same concepts as other object-oriented programming languages, it uses them to achieve very different goals. C++ uses objects to address the complexity problems inherent in large C applications written by teams of programmers. Smalltalk and Lisp use objects to make individual developers more productive. These highly interactive languages offer great value in exploratory programming and prototyping.

In its simplest form, OOP simply states that data structures should be treated more like objects in the real world. A data structure is much like a piece of paper. Our programs write notes on the paper, read the notes back, erase them, and write other notes.

Throughout this process there is a hidden assumption that the note will mean the same thing to its reader that it did to the writer. When different parts of a program interpret things differently we have problems. Frequently, these problems show up when different developers on the same project attempt to integrate their efforts.

By contrast, OOP associates a data structure with the set of operations which act upon it. Once these operations or methods (the "interface" to the object) have been defined, everyone is expected to use them to communicate with the object. This offers important benefits:

This is the data-abstraction concept, as supported by languages like Ada. OOP goes one step further by providing an inheritance mechanism. New classes of objects can be defined in terms of existing object classes. A class inherits all of the structure and behavior of its ancestor classes. It may choose to define additional structure and behavior of its own. It can also override any behavior inherited from any of its ancestors by defining a replacement version of the inherited operation.

At this point, object-oriented languages head off in a number of different directions. A few stop here, satisfied with the benefits they receive from more modular code. Most others provide a mechanism for selecting the appropriate method for an object at execution time. These languages permit a single piece of code to operate on data of many different types.

Smalltalk and the Common Lisp Object System (CLOS) go much further. In addition to allowing a class to replace inherited methods, they let it customize these methods through encapsulation. This is useful when a new class's behavior is a variation on that of its ancestors. Smalltalk's super-send mechanism lets a method perform some preprocessing, invoke the behavior it inherited from its ancestors, then do some additional processing, all without requiring a programmer to know precisely which ancestor provides the desired behavior. CLOS's :BEFORE, :AFTER, and :AROUND method types and the CALL-NEXT-METHOD procedure offer similar capabilities.

C++ lacks an equivalent to Smalltalk's super-send. Instead, it forces programmers to hard code the name of the class whose method is being encapsulated. For example, we might create an array class that implements resizable arrays of integers and a protected_array class based on the array class which adds a test for invalid subscripts. protected_array puts the subscript test in its subscript operator (the operator[] defined in Example 1). If the subscript is within range, the subscript operator on protected_array invokes the subscript it inherits from array, see Example 1.

Example 1: When subscripts are within range, the subscript operator invokes the subscript it inherits.

       int& protected_array::operator[] (int x)
       { return (x > = 0 && x < size()) ?
          array::operator[] (x) : error_value; }

In this example there is one parent class and one child, so having to refer to the parent class isn't a problem. It becomes steadily more serious as the number of classes in the inheritance tree grows. When one class has dozens of ancestors (not at all uncommon in Smalltalk and Lisp), it is too much to expect a programmer to know the origin of every single method. The situation becomes worse when a redesign of a set of classes makes it necessary to move methods from one class to another. Suddenly it becomes necessary to locate and modify every caller of the moved method.

This protected_array example illustrates another important difference between C++ and more exploratory languages like Smalltalk and Lisp: Programmers in these languages expect to have things like range-checked arrays provided for them. Smalltalk is known and loved as much for the size and richness of its class libraries as for the language itself.

C++: Why It Is The Way It Is

C++ began as "C with Classes," a set of object extensions to C. These extensions were based on the Simula67 language, the first language to exhibit characteristics which we would now call object oriented. In adding features to C, Bjarne Stroustrup was determined not to compromise its efficiency and expressiveness. He identified several important constraints for his new language:

C++'s handling of ambiguity also shows up in its method for selecting among overloaded functions. Consider the code in
Example 2, which defines two functions for finding the smaller of a pair of numbers.

Example 2: Defining two functions for finding the smaller of a pair of numbers.

     int min (int a, int b)
     { return a < b ? a : b; }

     double min (double a, double b)
     { return a < b ? a : b; }

     main ()
     { int x = min (5, 7);
       double y = min (3.2, 1.3);
       double z = min (16.3, 2); }

     % CC min. cc

     "min.cc", line 10: error: ambiguous call: min ( double , int )
     "min.cc", line 10: choice of min ()s:
     "min.cc", line 10:        int min(int , int );
     "min.cc", line 10:        double min(double , double );
     Compilation failed
     %

The first call to min uses the (int, int) version of min, since both arguments are of type int. The second call to min uses the (double, double) version for the same reason. The third call is ambiguous. Should the compiler cast the first argument to an int? Or should it cast the second to a double? C++ leaves the choice to the programmer, who must either cast one of the arguments or define a third version of min that takes a double and an int.

C++ demonstrates that the whole of a programming language can be far more complex than the sum of its parts. Experienced C programmers can learn all of the syntactic elements of the language and their semantics and usage in a couple of days. This will be followed by several months of discovering what happens when individual elements are combined. Can I combine overloaded and virtual-member functions? How about overloaded functions, template functions, and default values for function arguments? What happens if I provide operations to convert from a built-in data type to an object class and back again? How do I know that the compiler has written functions for me or generated function calls that don't appear in my source?

Taken as a whole, C++ seems more the product of engineering than of art. Given all the constraints on its design, perhaps that should not come as much of a surprise.

C++ is a Better C (and Isn't that Enough?)

One measure of an object-oriented language is its ability to support an object programming style. A pure object-oriented language should make it easy to program with objects and difficult to program any other way. By this measure, Smalltalk is the best object-oriented language available today. CLOS makes Lisp a good object language. Although programmers can use Lisp to write procedural code, it does give them plenty of reasons to use objects.

If C++ is a less-than-perfect object-oriented language by this measure, it is also true that most programmers aren't trying to use it as one. C++ satisfies a very real need for a better C language. C++ provides programmers with better compile-time error detection and allows them to be more explicit in describing their intentions than is possible in C.

The fact that so many of C++'s language elements can be used in isolation is to the advantage of the more pragmatic developer. Many C++ shops seem to settle on a subset of the language, whether as part of a formal process or through a combination of comfort and inertia. Although they may not outlaw the rest of C++, they don't feel an obligation to use features just because they exist.

Where might one divide the C++ language? Here's one possible set of layers:

Needless to say, there are many other ways to divide up the features of C++. The important point is not to think of C++ as just an object-oriented language. It is really a vast collection of extensions to C, some of which are based on object techniques. Its object features exist not to support the kind of programming-in-the-small style seen in Smalltalk, Lisp, and languages like Objective-C and Eiffel, but to address issues of integration and maintenance encountered by large teams of programmers working on major projects.

C++ is an object-oriented language that a C programmer can appreciate, especially the kind of C programmer who in an earlier age would have written in assembly language. It is oriented first toward execution performance and then toward flexibility. Most of the features that C++ adds to C involve no runtime overhead. Those few that do can be avoided by the efficiency-oriented programmer.

It is the modular nature of the elements of C++ that makes it both a weak object language and a strong general-purpose language. OOP purists may decry its limitations. More pragmatic developers are likely to decide that C++ is just object oriented enough to help them get their work done.


Copyright © 1992, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.