Let's start with a classmate's question on stackoverflow. The prototype of the problem is as follows (for the original problem, see class member privacy and headers in C ++ ):
Portaljacker: "There is a Class A with some common member functions and private data, as shown below ."
class A{public: X getX(); Y getY(); Z getZ(); ..private: X god; Y damn; Z it;};
"But I don't want users using this class to see my private data. What should I do? It may be because it is too difficult for others to laugh at the variable name! Ah, but what is it about them! I tried to put these three member variables into another header file, just like below, but the compiler reports an error-what should I do ?!"
// A.hclass A{public: X getX(); Y getY(); Z getZ();};// A.cppclass A{private: X god; Y damn; Z it;};
When I first saw this problem, I thought it was naive. First, the definition of a class in C ++ must contain all the member functions and variables of the class. Unlike different functions in a namespace, they can be freely distributed in different source files. What if these private members are visible to the caller? They are private, and users cannot directly access them.
However, I am wrong.
This requirement of portaljacker is actually quite reasonable. Imagine that callers generally use Class A in this way.
// main.cpp#include "A.h"int main(){ A a; X x = a.getX(); Y y = a.getY(); Z z = a.getZ(); .. return 0;}
Generally, the caller must include the header file of the definition of a before compilation can be successful. That is to say, a compilation dependency is established: Main. cpp-> A. H. In this way, any changes in the. h file will cause the main. cpp to be re-compiled, even if the changes are only private variables in Class A (such as name changes ). This is very bad because the private data of a class belongs to its implementation details (implementation details). Ideally, it should be hidden and its changes are invisible to callers. Oh, I wonder if you have ever encountered such a project, which contains hundreds of source files. You just changed a small header file and found that most files in the project were re-compiled, but they were not compiled in a few minutes.
In fact, portaljacker raised a very good question. The key to the problem is how to hide the implementation details. In this way, the caller will neither see any internal implementation of the class nor be forced to re-compile because of any changes in the implementation.
Before discussing the solution to the problem, it is necessary to look back at why portaljacker's approach does not work. He puts the definitions of the common and private member types of the same class into the definitions of two classes with the same name (see the above ).
I heard it, and you said no. That's right. Why ?" Because the class definition cannot be separated .. "Okay, but why ?" C ++ is like this, common sense! "Senior programmers may even go to a page of the C ++ standard," summary, this is the standard." Many of us (including me) know what a book (or teacher) says when learning a language, as long as they know how to use it correctly, few people will think about the reasons behind the rule.
Return to the topic. One of the major reasons why C ++ does not allow division of class definitions is that the size of objects needs to be determined during compilation. Considering the main function above, this Code cannot be compiled when the class definition is split. Because the compiler needs to know the size of object a when compiling "A a", and this information is obtained by viewing the definition of object. At this time, the private member of the class is not in it, and the compiler cannot determine the size of. Note that this problem does not exist in Java, because all Java objects are referenced by default, similar to pointers in C ++, and the object size does not need to be known during compilation.
Separation of interfaces and Implementations
Now let's go back to the problem to be solved:
- Users do not want to see the internal implementation of the class (for example, how many private data are there, what type they are, and what their names are ).
- Except interfaces, changes to any class should not cause the caller to recompile.
The solution to these problems is to properly hide the implementation. For integrity, let's take a look at several common interfaces and implementation separation technologies. Their support for Information Hiding is different, and not all of the above problems can be solved.
1. Use Private Members
Class interfaces are common, and all implementation details are private. This is also the essence of C ++ Object-oriented thinking. By encapsulating all implementations into private, the caller does not need to change any code when the class changes, unless the public interface of the class changes. However, such separation is only preliminary because it may cause the caller to re-compile, even if the common interfaces do not change.
# Include "x. H "# include" Y. H "# include" Z. H "Class A {// public: X getx (); y Gety (); Z Getz ();.. // partially implement Private: X God; y damn; Z it ;};
2. Dependency object declaration rather than definition)
In the previous method, classes A and X, Y, and Z are tightly coupled. If Class A uses a pointer rather than an object, Class A does not need to include the definitions of X, Y, and Z. It can be simply declared forward (Forward Declaration.
// A.hclass X;class Y;class Z;class A{public: X getX(); Y getY(); Z getZ(); ..private: X* god; Y* damn; Z* it;};
In this way, when X, Y, or Z changes, the caller (main. cpp) of A does not need to re-compile, which can effectively prevent cascading dependencies. In the previous method, if X is changed, all source files containing a. h need to be re-compiled. Note: When declaring a function, you do not need to define the corresponding class even if the parameter or return value of the function has been copied. (In the above example, x, y, Z header file ). It is required only when the function is implemented.
Iii. pimpl Mode
A better way is to "proxy" all the implementation details of a class to the other class, and provide the interface only. The interface is implemented by calling the corresponding functions of the impl class. Scott Meyers said this is"Truly separated interfaces and Implementations".
// Aimpl. hclass aimpl {public: X getx (); y Gety (); Z Getz ();.. PRIVATE: X; y; Z;}; //. hclass X; Class y; Class Z; Class aimpl; Class A {public: // possible implementation: X getx () {return pimpl-> getx ();} X getx () y Gety () Z Getz ();.. PRIVATE: STD: tr1: shared_ptr <aimpl> pimpl ;};
Let's take a look at whether this method can meet our two requirements. First, because any implementation details are encapsulated in the aimpl class, it is invisible to the call end. Second, the call end does not need to be re-compiled as long as the interface does not change. Good! Of course, there is no free lunch in the world, and this method also requires a price. The cost is that an additional aimpl class needs to be maintained, and each call to the interface will cause indirect calls to the corresponding aimpl interface. Therefore, if you encounter such a problem, think about the efficiency and Data encapsulation, which is more important to your code.
Iv. Interface Class
The other method that can meet both needs is to use an interface class, that is, an abstract class that does not contain private data. The caller first obtains the pointer to an aconcrete object, and then operates through the interface pointer. The cost of this method is that there may be one more vptr pointing to the virtual table.
// A.hclass A{public: virtual ~A(); virtual X getX() = 0; virtual Y getY() = 0; virtual Z getZ() = 0; ..};class AConcrete: public A{ ... };
Summary:
- Try to rely on object declarations rather than definitions. Such loose coupling can effectively reduce dependencies during compilation.
- It can completely hide the implementation of classes and reduce the compilation dependency in two ways: pimpl and interface.