Introduction to C++ file dependencies

  • 2020-04-01 21:28:40
  • OfStack

If you feel that the recompile time is too short or too long, because you need to recompile anyway, you can choose to skip this article, but it is recommended to browse.
If you want to learn or care about this topic, then this article is sure to be rewarding.
First of all, instead of defining dependencies, let me give you an example.


 class Peopel{
 public:
     People(const std::string & name,const Date& brithday,Image Img)
     std::string name( ) const;
     Date birthDate( ) const;
     Image img( ) const;
     ...
 private:
     std::string theName;               //The name
     Date theBirthDate;                 //birthday
     Image img;                         //The picture
 };

Class People cannot be compiled without the compiler knowing the definitions of the classes string,Date, and Image. Generally this definition is provided by the #include header file, so People generally have these preprocessing commands on them

  #include <string>
  #include "date.h"
  #inblude "image.h"
 class Peopel{
 public:
     People(const std::string & name,const Date& brithday,Image Img)
     std::string name( ) const;
     Date birthDate( ) const;
     Image img( ) const;
     ...
 private:
     std::string theName;               //The name
     Date theBirthDate;                 //birthday
     Image img;                         //The picture
 };

Then there is a compilation dependency between the People definition file and the three files. If any of these header files are changed, or if the header files depend on any other header file, then every file containing the People class needs to be recompiled, and using the People class file also needs to be recompiled. Consider that if a project contains a thousand files, each file contains several other files, and so on, changing the contents of a file, then you need to recompile almost the entire project, which is pretty bad.

We can make the following changes


 namespace std {
     class string;
 }
 class Date;
 class Image;

 class Peopel{
 public:
     People(const std::string & name,const Date& brithday,Image& Img)
    std::string name( ) const;
    Date birthDate( ) const;
    Image img( ) const;
    ...
private:
    std::string theName;                //The name
    Date theBirthDate;                 //birthday
    Image img;                         //The picture
};

This only recompiles People if the interface is changed, but there's a problem with that. The first thing is string is not class, it's a typedef. Char> String. Therefore, the preceding declaration is not correct (attached in the STL full code); , the correct preposition declaration is more complicated. In fact, for the standard library part, we just include it with the #include preprocessing command.

 #ifndef __STRING__
 #define __STRING__

 #include <std/bastring.h>

 extern "C++" {
 typedef basic_string <char> string;
 // typedef basic_string <wchar_t> wstring;
 } // extern "C++"
#endif

Another problem with predeclaration is that the compiler must know the size of the object at compile time in order to allocate space.
Such as:

  int main(int argv,char * argc[ ])
    {
        int x;
        People p(  parameter  );
        ...
    } 

When the compiler sees the definition of x, it knows how much memory must be allocated, but it doesn't know when it sees the definition of p. But if set to pointer, it is clear, because the pointer itself is known by the site translator.

#include <string>
#include <memory>
class PeopleImpl;
class Date;
class Image;
class People{
public:
   People(const std::string & name, const Date& brithday, const Image &Img);
   std::string name( ) const;
   Date birthDate( ) const;
   Imge img( ) const;
   ...
private:
   PeopleImpl * pImpl;
}

PeopleImpl contains the following three data, and the member variable pointer to People points to this PeopleImpl, so now the compiler knows the size of its allocated space from the People definition, the size of a pointer.

 public PeopleImpl
 {
     public:
         PeopleImple(...)
         ...
     private:
         std::string theName;                //The name
         Date theBirthDate;                 //birthday
         Image img;                         //The picture
 } 

In this way, People is completely separated from the implementation of Date, Imge, and People, and any changes to the above classes do not require recompiling the People file. Also, writing this way enhances encapsulation. This also reduces file dependencies.
Here is a summary of the methods to reduce dependency:

1. Don't use class definitions if you can declare them.
2. The data is represented by a pointer to the data.
3. Provide different header files for declarative and defined.
The two files must be consistent, and if a declaration is changed, both files must be changed. Therefore, it is common to have a #include declaration file instead of predeclaring several functions.
Like People do  


 #include "People.h"
 #include "PeopleImpl.h"

 People::People(const std::string& name, const Date& brithday, const Image& Img)
 :pImpl(new PersonImpl(name,brithday,addr))
 { }
 std::string People::name( ) const
 {
     return pImpl->name( );
 }

The other type of Handle is to make People a special abstract base class called Interface. Maybe those of you who are familiar with C# and Java have already seen the word interface. This interface has no member variables, no constructors, only a virtual destructor, and a set of purely virtual functions that represent the entire interface. So the interface class for People looks like this.

 class People{
 public:
     virtual ~People( );
     virtual std::string name( ) const = 0;
     virtual Date brithDate( ) const =0;
     virtual Image address( ) const =0;
     ...
 };

How do you create an object? They usually call a special function. Such functions are often called factory functions or make-believe functions. They return Pointers to dynamically allocated objects that support the interface of the interface class.

   class People {
     public:
         ...
         static People* create(const std::string& name,const Date& brithday, const Image& Img);
     };

The class that supports the interface class must be defined, and the actual constructor must be called

 class RealPeople:public People{
 public:
     RealPeople(const std::string& name,const Date& birthday,const Image& Img)
     :theName(name),theBrithDate(brithday),theImg(Img)
 {}
     virtual ~RealPeople() { }
     std::string name( ) const;
     Date birthDate( ) const;
     Image img( ) const;
 private:
     std::string theName;
     Date theBirthDate;
     Image theImg;
 }

With the RealPeople class, we can write People::create like this

 People* People::create(const std::string& name, const Date& birthday, const Image& Img)
 {
     return static_cast<People *>(new RealPerson(name,birthday,Img));
 }

The Handle and interface classes decouple the interface from the implementation, thus reducing the compilation dependency between files. But it also loses some performance and space.


Related articles: