Implementations, Notes(5), Effective C++

Implementations

(I read a Chinese version of the book, any translation problem plz point out.

Postpone variable definitions as long as possible until initial values can be provided

Minimize casting

  • old-style casts
    • C style cast: (T)expression
    • function style cast: T(expression)
  • CPP style casts
    • const_cast: cast away the constness.
    • dynamic_cast: safe downcasting, to decide which type a object belongs to in the inheritance hierarchy.
    • reinterpret_cast: low-level cast.
    • static_cast: force implicit conversions.

tips:

  • dynamic_casts maybe implement based on the comparision on string names of classes.
  • try to hide the cast in a function, then provide to customers, once it is a must.
  • do use the CPP style cast.

Avoid returning “handles” to object internals

handles: references, pointers, iterators
try to use a copy

class GUIObject { ... };
const Rectangle boundingBox(const GUIObject& obj);

GUIobject* pgo;

// The temporary Point is destructed when the statement is over, 
// and the pUpperLeft is dangling.
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());

Strive for exception-safe code

when exception throws, exception-safe code will guarantee:

class PrettyMenu {
public:
    void changeBackground(std::istream& imgSrc);
private:
    Mutex mutex;
    Image* bgImage;
    int imageChanges;
};


void PrettyMenu::changeBackground(std::istream& imgSrc) {
    lock(&mutex);
    delete bgImage;
    ++imageChanges;
    bgImage = newImage(imgSrc);
    unlock(&mutex);
}
  • no resources leak.
    if new Image(imgSrc) throws, mutex will never release.
  • no data break.
    if new Image(imgSrc) throw, bgImg will point to a deleted object.
    and the behavior of imageChanges will be weird, which it changed or not.

exception-safe functions provide 3 guarantees:

  • basic promise.
    when exception throws, everything is in a valid state, but the users can’t predict it.
  • strong guarantee.
    when exception throws, the program remains unchanged.
  • nothrow guarantee
    int doSomething() throw();
    though the function throws nothing, it is not guaranteed. aka., if throws, it will be a fatal error, unexpected will be called (Google set_unexpected for detail).
    all the properties of the functions is decided by implementation, not the declaration.

copy and swap

// pimpl idiom
struct PMImpl {
    std::shared_ptr<Image> bgImage;
    int imageChanges;
};

class PrettyMenu {
private:
    Mutex mutex;
    std::shared_ptr<PMImpl> pImpl;
}

void PrettyMenu::changeBackground(std::istream& imgSrc) {
    using std::swap;
    Lock m1(&mutex); // Use resource-managing class to manipulate mutex
    std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // Copy
    pNew->bgImage.reset(new Image(imgSrc)); // Update the copy
    swap(pImpl, pNew); // Swap
}
  • even if strong guarantee can be implemented by copy and swap, but it is not implementable to all the functions. and watch the cost of copy.
  • exception-safety is decided by the weakest one of all the functions.
  • once you have no choice but to set no any guarantee (when you call the old code), document it.

Understand the ins and outs of inlining

  • inline just applys for inling to compiler, not forcing to do so.
  • inline the minimal, frequently called functions.
  • inline functions usually put in headers. because inlining happens in compiling period, compiler needs to know what they look like.
  • the functions calls virtual functions will be never inlined, because virtual functions can only be known in run-time.
  • think over before inling construtors and destrutors. maybe there are complicated codes generated by compilers.

Minimize compilation dependencies between files

pimpl idiom -> handle classes

// Person.h
#include <string>

#include "datefwd.h"
#include "addressfwd.h"

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::shared_ptr<PersonImpl> pImpl;
};

// Person.cpp
#include "Person.h"
#include "PersonImpl.h"

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(); 
}

abstract base class -> interface class

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

    static std::shared_ptr<Person> create(const std::string& name, 
                                            const Date& birthday, const Address& addr);
};

class RealPerson: public Person {
public:
    RealPerson(const std::string& name, const Date& birthday, const Address& addr)
                : name(name), birthday(birthday), addr(addr) {}
    virtual ~RealPerson() {}
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
};

std::shared_ptr<Person> create(const std::string& name, 
                                const Date& birthday, const Address& addr) {
    return std::shared_ptr<Person>(new RealPerson(name, birthday, addr)); 
}
  • don’t use objects, if object references or object pointers achieves.
  • replace class definitions with class declarations as much as possible.
  • provide declaration and definition with different headers.

use handle classes and interface classes to minimize changes for users during the develpment of program.
but replace with concrete classes, when the great difference in speed or size compared to coupling between classes.