Discussion
Team LiB
Previous Section Next Section

Discussion

When it makes sense to create a "compiler firewall" that completely insulates calling code from a class's private parts, use the Pimpl idiom: Hide them behind an opaque pointer (a pointer, preferably an appropriate smart pointer, to a class that is declared but not yet defined). For example:



class Map {


 // …


private:


 struct  Impl;


 shared_ptr<Impl> pimpl_;


};



The eponymous Pimpl should be used to store all private members, both member data and private member functions. This allows you to make arbitrary changes to your class's private implementation details without any recompilation of calling codean independence and liberty that is the hallmark of the idiom. (See Item 41.)

Note: Do declare the Pimpl using two declarations as shown. It would be legal, but have a different meaning, to combine the two lines and forward-declare the type and a pointer to it in one statement with struct Impl* pimpl;but then Impl is in the enclosing namespace and not a nested type within your class.

There are at least three reasons you might Pimpl, and they all stem from C++'s distinction between accessibility (whether you can call or use something) and visibility (whether you can see it and therefore depend on its definition). In particular, all private members of a class are inaccessible outside member functions and friends, but are visible to the entire worldto all code that sees the class's definition.

The first consequence of this is potentially longer build times due to processing unnecessary type definitions. For private data members held by value, and for private member functions' parameters taken by value or used in visible function implementations, types must be defined even if they can never be needed in this compilation unit. This can lead to longer build times. For example:



class C {


 // …


private:


 AComplicatedType act_;


};



The header file containing class C's definition must also #include the header containing the definition for AComplicatedType, which in turn transitively includes every header that AComplicatedType might need, and so on. If the headers are extensive, compilation times can be noticeably affected.

The second consequence is creating ambiguity and name hiding for code that is trying to call a function. Even though private member functions can never be called from outside the class and its friends, they do participate in name lookup and overload resolution and so can render calls invalid or ambiguous. C++ performs name lookup and then overload resolution before accessibility checking. That's why visibility gets priority:



int Twice( int );             // 1





class Calc {


public:


 string Twice( string );      // 2





private:


 char* Twice( char* );        // 3





 int Test() {


  return Twice( 21 );      // A: error, 2 and 3 are unviable (1 would be viable,


 }                            // but it can't be considered because it's hidden)


};





Calc c;


c.Twice( "Hello" );       // B: error, 3 is inaccessible (2 would be fine, but it


                              // can't be considered because 3 is a better match)



On line A, the workaround is to explicitly qualify the call as ::Twice( 21 ) to force lookup to select the global function. On line B, the workaround is to add an explicit cast as c.Twice( string("Hello") ) to force overload resolution to select the intended function. Some of these calling issues can be worked around in other ways than the Pimpl idiom, for example by never writing private overloads for member functions, but not all of the issues resolves by Pimpl have such alternative workarounds.

The third consequence is its impact on error handling and safety. Consider Tom Cargill's Widget example:



class Widget {// …


public:


 Widget& operator=( const Widget& );





private:


 T1 t1_;


 T2 t2_;


};



In short, we cannot write operator= to give the strong guarantee or even the minimum required (basic) guarantee if T1 or T2 operations might fail in a way that is not reversible (see Item 71). The good news is that the following simple transformation always works to enable at least the basic guarantee for error-safe assignment, and usually the strong guarantee as long as the needed T1 and T2 operations (notably construction and destruction) don't have side effects: Hold the member objects by pointer instead of by value, preferably all behind a single Pimpl pointer.



class Widget {// …


public:


 Widget& operator=( const Widget& );





private:


 struct Impl;


 shared_ptr<Impl> pimpl_;


};





Widget& Widget::operator=( const Widget& ) {


 shared_ptr<Impl> temp( new Impl( /*...*/ ) );





 // change temp->t1_ and temp->t2_; if it fails then throw, else commit using:





 pimpl_ = temp;


 return *this;


}



    Team LiB
    Previous Section Next Section