In a already old article "The Joy of Pimpls (or, More About the Compiler-Firewall Idiom)", Herbe Sutter states
There are four main alternative disciplines:
- Put all private data (but not functions) into XImpl.
- Put all private members into XImpl.
- Put all private and protected members into XImpl.
- Make XImpl entirely the class that X would have been, and write X as only the public interface made up entirely of simple forwarding functions (another handle/body variant).
My experience is that in some contexts we could add more private member functions than private member data. For example, when we apply the pattern "Methods for States - A Pattern for Realizing Object Lifecycles" from Kevlin Henney, we have a lot of internal function to represent transitions between states. A change in the implementation of the FSM means adding or removing transition. If these internal functions are declared at the class level, we need to modifying the header file.
I have explored a different variant
- Put all private functions and static data (but not instance data) into XImpl. XImpl needs only to store the back pointer.
This variant avoid changing the header file in those cases. I will adapt the clock example from "Methods for States" to show how the variant works.
// clock.hpp class clock { public: void change_mode() { (this->*(behavior->change_mode))(); } void increment() { (this->*(behavior->increment))(); } void tick() { (this->*(behavior->tick))(); } private: typedef void (clock::*function)(); struct mode { const function change_mode, increment, tick; }; static const mode displaying_time static const mode setting_hours; static const mode setting_minutes; const mode *behavior; int hour, minute, second; template<const mode *next_mode> void change_to() { behavior = next_mode; } void next_hour() { hour = (hour + 1) % 24; } void next_minute() { minute = (minute + 1) % 60; } void update_time() { if(++second == 60) { second = 0; if(++minute == 60) { minute = 0; hour = (hour + 1) % 24; } } } void do_nothing() {} }; // clock.cpp const clock::mode clock::displaying_time = { &clock::change_to<&setting_hours>, &clock::do_nothing, &clock::update_time }; const clock::mode clock::setting_hours = { &clock::change_to<&setting_minutes>, &clock::next_hour, &clock::do_nothing }; const clock::mode clock::setting_minutes = { &clock::change_to<&displaying_time>, &clock::next_minute, &clock::do_nothing };Hiding internal functions and static data in the header file
Only the public interface, the private instance data and the grant friendship to impl appear on the header file.
// clock.hpp class clock { public: void change_mode(); void increment(); void tick(); private: struct impl; // forward declaration friend class impl; struct mode; // forward declaration const mode *behavior; int hour, minute, second; };Moving to the implementation file the private part
The types used only by the implementation go to the implementation file clock.cpp.
// clock.cpp typedef void (clock::impl::*function)(); struct clock::mode { const function change_mode, increment, tick; };The class implementation follows always the same schema.
- Define a variable referencing the class to implement, and
- a constructor taking an instance of this class.
struct clock::impl { clock& that; impl(clock* thisClock) : that(*thisClock) {} ... };Next follows the private static data:
struct clock::impl { ... static const clock::mode displaying_time; static const clock::mode setting_hours; static const clock::mode setting_minutes; ... };And the functions that were private
struct clock::impl { ... template<const clock::mode *next_mode> void change_to() { that.behavior = next_mode; } void next_hour() { that.hour = (that.hour + 1) % 24; } void next_minute() { that.minute = (that.minute + 1) % 60; } void update_time() { if(++that.second == 60) { that.second = 0; if(++that.minute == 60) { that.minute = 0; that.hour = (that.hour + 1) % 24; } } } void do_nothing() {} };The definition of the public functions need just to replace this-> by impl(this).
void clock::change_mode() { (impl(this).*(behavior->change_mode))(); } void clock::increment() { (impl(this).*(behavior->increment))(); } void clock::tick() { (impl(this).*(behavior->tick))(); }Last, the static initialization is done.
const clock::mode clock::impl::displaying_time = { &clock::impl::change_to<&setting_hours>, &clock::impl::do_nothing, &clock::impl::update_time }; const clock::mode clock::impl::setting_hours = { &clock::impl::change_to<&setting_minutes>, &clock::impl::next_hour, &clock::impl::do_nothing }; const clock::mode clock::impl::setting_minutes = { &clock::impl::change_to<&displaying_time>, &clock::impl::next_minute, &clock::impl::do_nothing };While this approach doesn't encapsulates all the private members, it has some advantages:
- The XImpl class has no inherent instance data, so no space overhead
- The class doesn't needs to store any XImpl pointer as there is no data to maintain, so no need to allocate/deallocate it.
- Reduce the performance overhead of the Pimpl idiom.
- Need to include the headers declaring the private instance data types.
- There is yet a minimal performance overhead on the construction of the temporary XImpl class and the dereference of the back pointer.
No comments:
Post a Comment