Sponsored Link •
Scott Meyers, C++ expert and author of numerous books, including Effective C++, talks with Bill Venners about the importance of saying what you mean and understanding what you say, the three fundamental relationships between classes, and the difference between virtual and non-virtual functions.
Scott Meyers is one of the world's foremost experts on C++ software development. He wrote the best-selling Effective C++ series (Addison-Wesley): Effective C++ (1997), More Effective C++ (1995), and Effective STL (2001); wrote and designed the Effective C++ CD; is consulting editor for Addison-Wesley's Effective Software Development Series; and is a member of the advisory board for Software Development magazine. A programmer since 1972, Meyers holds an M.S. in computer science from Stanford University and a Ph.D. from Brown University.
In this four-part interview, which is being published in weekly installments, Meyers gives his views on many object-oriented design topics:
interface, and points out a schism in the focus of the C++ and other prominent development communities.
Bill Venners: One guideline you include in Effective C++ is, "Say what you mean; understand what you're saying." You make the point that it's not enough to understand language syntax. You have to understand what you mean when you use the syntax. Why do you think that is important?
Scott Meyers: You are communicating with two different parties
when you write a program: the compiler and other human beings. The language behaves
in particular ways because constructs have certain kinds of meaning. For example, when I
say public inheritance means IS-A—as in an
ArrayList IS A
List—that is reflected in the language's explicit conversion rules. If
you're trying to make public inheritance mean something other than IS-A, you're then
fighting the language, because it will probably perform conversions that your semantic
model would not normally allow. If you want to represent something using a language
mechanism, and you don't understand to what the language mechanism corresponds, the
language will not enforce the rules that you want enforced for the particular semantics
that you need. One reason understanding meaning is important, therefore, is you are
communicating with the compiler and you don't want to fight the language. You want the
language to work for you and back up your semantics.
You also want to communicate with other human beings. As a human being, if I'm reading your code and I see that a class publicly inherits from that class, I will think, "This is an IS-A relationship." If that's not what you meant, I'll be confused. Or if I see a non-virtual member function, I will think, "This is an invariant. Nobody should ever override this." If I see it has been overridden in a derived class, I see a contradiction in your design. I no longer trust my understanding of your design, because I'm getting conflicting pieces of information. That makes it more difficult for me to understand what your program is supposed to do. So meaning is important because first, you don't want to ascribe semantics to a language feature that are different from what the compiler will enforce. And second, you don't want to miscommunicate what you're trying to express to other people who read your code.
Bill Venners: In Effective C++, you state that the
three common relationships between classes are IS-A, HAS-A, and IS-IMPLEMENTED-
IN-TERMS-OF. IS-A really helped me understand inheritance. HAS-A, as in a
Person HAS A
Name, didn't really help me much to
understand layering—what I usually call composition. You're other layering
relationship, IS-IMPLEMENTED-IN-TERMS-OF, although a mouthful, made sense to
me. Is that still how you describe the different ways to related classes? Has anything
changed in the last 10 years in how you would describe the basic class relationships?
Scott Meyers: In terms of the vocabulary I introduce in the book, nothing has changed. In fact, I didn't change any vocabulary in the second edition of Effective C++. I still think of those terms as the three fundamental relationships between classes.
Bill Venners: Please differentiate HAS-A and IS- IMPLEMENTED-IN-TERMS-OF.
Scott Meyers: When you write software, you deal with two
worlds. You deal with the world you want to model, the outside world. You also deal
with a world that exists only inside the software, which involves just getting the code to
work. HAS-A corresponds to something in the real world. A
Wheels or a
corresponds to the application domain. IS-IMPLEMENTED-IN-TERMS-OF never exists
in the real world; it is part of the implementation domain.
So you couldn't say a
Wheels. But you
could say the
ParkingLot IS-IMPLEMENTED-IN-TERMS-OF a
List. There's no
List in the real world. The
List only exists inside the software. So HAS-A is a relationship between
classes that exists in the application domain. IS-IMPLEMENTED-IN-TERMS-OF is a
relationship between classes that exists in the implementation domain.
Bill Venners: That's great. That clarifies it for me.
Scott Meyers: It took me a few years to figure that out myself, actually.
Bill Venners: This past spring I attended a .Net seminar at Microsoft. It was my first visit to Microsoft. I picked up many interesting subtexts, one of which concerned pragmatism versus idealism. One Microsoft manager said to me, "Microsoft is a company of pragmatists." I sensed a somewhat dismissive attitude at Microsoft of people who were too academic or idealistic. At times I got the sense they felt Java tries to be too pure or academic, and that C# is more pragmatic.
One specific example of this pragmatic attitude involved virtual and non-virtual functions. In Java, every instance method is dynamically bound, or virtual in C++ jargon. You can mark an instance method final in Java, in which case subclasses can't override it. But there's really nothing like a non-virtual instance method in Java.
In C++ and C#, you have a choice between virtual and non-virtual member functions.
In both languages, the default is non-virtual. You have to specifically mark the function
virtual keyword to achieve dynamic binding. Anders Hejlsberg,
C#'s main designer, claimed that making virtual the default, as it is in Java, is wrong. I'm
curious what you think of that philosophical difference between the Java designers and
the C# designers. What do you think should be the default and why? Also, how would
you suggest designers using C++ and C# choose between virtual and non-virtual
Scott Meyers: Let me answer the second question first. A non- virtual function is an assertion. It asserts that this method applies to this class and any derived classes that may exist; furthermore, you have to use this implementation. It asserts that if this implementation is not appropriate, you should not inherit from this class. A non-virtual function adds a new constraint to derived classes. It says, you have to do this, and you have to do it this way. If you don't want to do it this way, you can't inherit from me. You're not an IS-A relationship with me anymore.
Bill Venners: That's what final does in Java. When you add final to the method, subclasses can't override it. It's the same constraint.
Scott Meyers: It's exactly the same constraint. Unlike Java, C++ has no language mechanism to enforce that constraint. In Java, if you then try to override a final method in a derived class, the compiler would reject the code. C++ can express the constraint. That's what a non-virtual function is. But C++ doesn't have a language mechanism to enforce the constraint. You can play stupid C++ tricks and enforce it, but no one does that in practice.
A virtual function means the interface to the function applies to this class. By interface, I mean the function name, the types and order of the parameters, the return type, and most importantly, the semantics. A virtual function's interface applies to this class and all derived classes. That will never change. However, the implementation, if one is provided, because it could be a pure virtual function, is a default implementation. It may be overridden if you want to accomplish exactly the same thing in a different way. Because what the function accomplishes is fixed. That's the semantics. You can't play games with the semantics. If you do, you violate the IS-A relationship.
A non-virtual function says, you have to do this and you must do it this way. A virtual function says you have to do this, but you don't have to do it this way. That's their fundamental difference.
Regarding the C# decision that the default should be non-virtual rather than virtual, I can't put words in the designer's mind. I can tell you exactly why it's the default in C++.
Bill Venners: Why is non-virtual the default in C++?
Scott Meyers: Because it's more efficient. Because it's not dynamically bound, right? More important, this means that if you have a class in C++, and all member functions are non-virtual, then you will not have a vtbl (virtual table) or a vptr (virtual table pointer). So the object, as well as your overall application data space, is smaller. The decision to make functions non-virtual by default strongly ties in with the concern for efficiency.
The decision to make functions non-virtual by default in C++ also ties in with the concern for C compatibility. In C++, certain member functions are automatically generated, even in structs. You get an assignment operator if you ever do an assignment. You get a copy constructor if you make a copy. You get a destructor if you ever create an instance, at least on the stack or statically. And it is important that those functions are non-virtual, because if they were virtual, then you would change the object's layout and it would no longer be compatible with C.
Effective C++, Second Edition, by Scott Meyers is at Amazon.com at:
More Effective C++, by Scott Meyers is at Amazon.com at:
Effective STL, by Scott Meyers is at Amazon.com at:
Effective C++ CD, by Scott Meyers is at Amazon.com at:
Scott Meyer's website contains links to many publications, presentations, books, and other information from Scott:
Images of Persephone, the best dog in the world: