Effective C++ 笔记 —— Item 31: Minimize compilation dependencies between files.

C++ doesn't do a very good job of separating interfaces from implementations. A class definition specifies not only a class interface but also a fair number of implementation details. For example:

class Person 
{
public:
    Person(const std::string& name, const Date& birthday, const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
    // ...
private:
    std::string theName; // implementation detail
    Date theBirthDate; // implementation detail
    Address theAddress; // implementation detail
};

Here, class Person can't be compiled without access to definitions for the classes the Person implementation uses, namely, string, Date, and Address. Such definitions are typically provided through #include directives, so in the file defining the Person class, you are likely to find something like this:

#include <string>
#include "date.h"
#include "address.h"

Unfortunately, this sets up a compilation dependency between the file defining Person and these header files. If any of these header files is changed, or if any of the header files they depend on changes, the file containing the Person class must be recompiled, as must any files that use Person. Such cascading compilation dependencies have caused many a project untold grief.

You might wonder why C++ insists on putting the implementation details of a class in the class definition. For example, why can't you define Person this way, specifying the implementation details of the class separately?

namespace std 
{
    class string; // forward declaration (an incorrect one — see below)
} 

class Date; // forward declaration
class Address; // forward declaration
class Person 
{
public:
    Person(const std::string& name, const Date& birthday,
    const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
    // ...
};

There are two problems with this idea:

  • string is not a class, it's a typedef (for basic_string). As a result, the forward declaration for string is incorrect. The proper forward declaration is substantially more complex, because it involves additional templates. That doesn't matter, however, because you shouldn't try to manually declare parts of the standard library. Instead, simply use the proper #includes and be done with it. Standard headers are unlikely to be a compilation bottleneck, especially if your build environment allows you to take advantage of precompiled headers. 
  • Compilers need to know the size of objects during compilation.The only way they can get that information is to consult the class definition, but if it were legal for a class definition to omit the implementation details, how would compilers know how much space to allocate?

This question fails to arise in languages like Smalltalk and Java, because, when an object is defined in such languages, compilers allocate only enough space for a pointer to an object. That is, they handle the code above as if it had been written like this:

int main()
{
    int x; // define an int
    Person *p; // define a pointer to a Person ...
}

This, of course, is legal C++, so you can play the "hide the object implementation behind a pointer" game yourself. One way to do that for Person is to separate it into two classes, one offering only an interface, the other implementing that interface. If the implementation class is named PersonImpl, Person would be defined like this:

#include <string> // standard library components shouldn't be forward-declared
#include <memory> // for tr1::shared_ptr; see below

class PersonImpl; // forward decl of Person impl. class
class Date; // forward decls of classes used in
class Address; // Person interface

class Person 
{
public:
    Person(const std::string& name, const Date& birthday, const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
    // ...
private: // ptr to implementation;
    std::tr1::shared_ptr<PersonImpl> pImpl; // see Item 13 for info on
};

With this design, clients of Person are divorced from the details of dates, addresses, and persons. The implementations of those classes can be modified at will, but Person clients need not recompile. In addition, because they're unable to see the details of Person’s implementation, clients are unlikely to write code that somehow depends on those details. This is a true separation of interface and implementation.

The key to this separation is replacement of dependencies on definitions with dependencies on declarations. That’s the essence of minimizing compilation dependencies: make your header files self-sufficient whenever it's practical, and when it's not, depend on declarations in other files, not definitions. Everything else flows from this simple design strategy. Hence:

  • Avoid using objects when object references and pointers will do.
  • Depend on class declarations instead of class definitions whenever you can.
  • Provide separate header files for declarations and definitions.

Classes like Person that employ the pimpl idiom are often called Handle classes. Lest you wonder how such classes actually do anything, one way is to forward all their function calls to the corresponding implementation classes and have those classes do the real work. For example, here's how two of Person’s member functions could be implemented:

#include "Person.h" // we’re implementing the Person class, so we must #include its class definition

#include "PersonImpl.h" // we must also #include PersonImpl's class definition, otherwise we couldn't call its member functions; note that PersonImpl has exactly the same public member functions as Person — their interfaces are identical

Person::Person(const std::string& name, const Date& birthday, const Address& addr)
: pImpl(new PersonImpl(name, birthday, addr))
{
}

std::string Person::name() const
{
    return pImpl->name();
}

An alternative to the Handle class approach is to make Person a special kind of abstract base class called an Interface class. The purpose of such a class is to specify an interface for derived classes (see Item 34). As a result, it typically has no data members, no constructors, a virtual destructor (see Item 7), and a set of pure virtual functions that specify the interface.

An Interface class for Person could look like this:

class Person 
{
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
    // ...
};

Clients of an Interface class must have a way to create new objects. They typically do it by calling a function that plays the role of the constructor for the derived classes that are actually instantiated. Such functions are typically called factory functions (see Item 13) or virtual constructors. They return pointers (preferably smart pointers — see Item 18) to dynamically allocated objects that support the Interface class’s interface. Such functions are often declared static inside the Interface class:

class Person 
{
public:
    // ...
    static std::tr1::shared_ptr<Person> // return a tr1::shared_ptr to a new
    create(const std::string& name, // Person initialized with the
    const Date& birthday, // given params; see Item 18 for
    const Address& addr); // why a tr1::shared_ptr is returned ...
};

Clients use them like this:

std::string name;
Date dateOfBirth;
Address address;
...
// create an object supporting the Person interface
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
...
std::cout << pp->name() // use the object via the
<< " was born on " // Person interface
<< pp->birthDate()
<< " and now lives at "
<< pp->address();
// ... // the object is automatically deleted when pp goes out of scope — see Item 13

For example, the Interface class Person might have a concrete derived class RealPerson that provides implementations for the virtual functions it inherits:

class RealPerson: public Person 
{
public:
    RealPerson(const std::string& name, const Date& birthday, const Address& addr)
        : theName(name), theBirthDate(birthday), theAddress(addr)
    {}

    virtual ~RealPerson() {}

    std::string name() const; // implementations of these
    std::string birthDate() const; // functions are not shown, but
    std::string address() const; // they are easy to imagine

private:
    std::string theName;
    Date theBirthDate;
    Address theAddress;
};

Given RealPerson, it is truly trivial to write Person::create:

std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr)
{
    return std::tr1::shared_ptr<Person>(new RealPerson( name, birthday, addr));
}

Handle classes and Interface classes decouple interfaces from implementations, thereby reducing compilation dependencies between files.

Things to Remember:

  • The general idea behind minimizing compilation dependencies is to depend on declarations instead of definitions. Two approaches based on this idea are Handle classes and Interface classes.
  • Library header files should exist in full and declaration-only forms. This applies regardless of whether templates are involved.
原文地址:https://www.cnblogs.com/zoneofmine/p/15252638.html