Other names:
Bridge, Firewall Compilation, Handle / BodySuppose we need to write a cross-platform network application using sockets. To do this, we need the GeneralSocket class (“Visible class”), which will encapsulate in itself the implementation details of a specific platform (“Hidden class”). It is often required to hide implementation details from users or other developers:
- In order to be able to change the implementation of the hidden class without recompiling the rest of the code, since the closed members are not accessible from the outside to anyone except member functions and friends, but are visible to everyone who has access to the class definition. Changing the class definition makes it necessary to recompile all users of the class.
- To hide names from scope. Although private members cannot be called by code outside the class, they nevertheless participate in the search for names and resolving overloads.
- To speed up build time, as the compiler does not need to handle extra definitions of closed types.
Consider an example:
//GeneralSocket.h
#include “UnixSocketImpl.h”
Class GeneralSocket{
public :
connect();
private :
UnixSocketImpl socket;
}
//GeneralSocket.cxx
GeneralSocket::connect(){
socket.connectImpl();
}
* This source code was highlighted with Source Code Highlighter .
In this example, it is necessary that the description of the hidden UnixSocketImpl class be known at the compilation stage. In addition, nothing prevents the user from using the functions of the UnixSocketImpl class bypassing the visible GeneralSocket class. Now we will try to replace the private member of the visible class with a pointer and remove the description of the hidden UnixSocketImpl class from the header file:
//GeneralSocket.h
Class UnixSocketImpl;
Class GeneralSocket
{
public :
GeneralSocket();
void connect();
private :
UnixSocketImpl * socket;
}
//GeneralSocket.cxx
#include “UnixSocketImpl.h”
GeneralSocket::GeneralSocket() : socket ( new UnixSocketImpl){}
GeneralSocket::~GeneralSocket() {
delete socket;
socket = 0;
}
void GeneralSocket::connect() {
socket->connectImpl();
}
* This source code was highlighted with Source Code Highlighter .
')
We managed to get rid of UnixSocketImpl.h in the header file and transfer it to the implementation file of the GeneralSocket class. Now the user will not be able to get to a specific implementation, and will use the functionality only through the interface of the GeneralSocket class.
In C ++, in case of changes in the class (even in private member functions), all users of this class must be recompiled. To avoid such dependencies, a pointer to functions is used, the members whose implementation should be hidden. This technique is called Pimpl (Pointer to Implementation - a pointer to the implementation). Two major drawbacks are as follows:
- Each object creation requires dynamic memory allocation for the object referenced by the pointer.
- Using several levels of indirection (at least one) to access members of a hidden object
What can you try to hide?
- Only hidden data members
- All hidden member data and member functions. Unfortunately, it is impossible to hide a virtual function, since it must be visible for derived classes. Also in a private class you may need a reference to an open class to use its functions.
- Closed and protected members. Unfortunately, protected members cannot be hidden because they must be accessible to derived classes.
- Whole class. The advantage is that the closed class does not need a pointer to an open class. On the other hand, we lose our inheritance
Now let's complicate the task a bit. Imagine that our class is used very often, and, as you know, dynamic heap allocation is a very expensive operation. Let's try to pre-allocate memory for the hidden object:
//GeneralSocket.h
Class GeneralSocket
{
public :
GeneralSocket();
void connect();
private :
static const size_t sizeOfImpl = 42; /* or whatever space needed*/
char socket [sizeOfImpl];
}
//GeneralSocket.cxx
#include “UnixSocketImpl.h”
GeneralSocket::GeneralSocket() : {
assert(sizeOfImpl >= sizeof (UnixSocketImpl));
new (&socket[0]) UnixSocketImpl;
}
GeneralSocket::~GeneralSocket() {
(reinterpret_cast<UnixSocketImpl *> (&socket[0]))->~UnixSocketImpl();
}
GeneralSocket::connect() {
socket->connectImpl();
}
* This source code was highlighted with Source Code Highlighter .
We managed to get rid of the declaration of the UnixSocketImpl class in the GeneralSocket header file, and get rid of dynamic memory allocation. Instead, we received a number of significant drawbacks:
- C ++ is a strongly typed language, and this trick is an attempt to circumvent the limitations of the language.
- Memory alignment problems. This method does not guarantee that the memory will be properly aligned for all members of UnixSocketImpl. A solution that does not guarantee complete portability, but still works in most cases - using union:
union max_align {
void * dummy1;
int dymmy2;
}
union {
max_align m;
char socket [sizeOfImpl];
}
* This source code was highlighted with Source Code Highlighter .
- Careful handling of UnixSocketImpl functions. For example, you now need to use your own assignment copy operator, or prohibit the default statement.
- Support for the GeneralSocket class has become more time consuming. Now it is necessary to keep the size sizeOfImpl current.
- Inefficient memory consumption in case the condition sizeOfImpl> = sizeof (UnixSocketImpl) is not met
As you know, allocating a fixed-size memory is much faster than allocating an arbitrary amount of memory. Let's try to use our memory allocator:
//GeneralSocket.h
class GSimpl;
class GeneralSocket {
private :
GSimpl * pimpl;
}
//GeneralSocket.cxx
#include “UnixSocketImpl.h”
class FixedAllocator {
public :
static FixedAllocator* Instance();
void * Allocate(size_t);
void Deallocate( void *);
private :
/*Singleton implementation that allocates memory of fixed size*/
};
struct FastPimpl {
void * operator new ( size_t s) {
return FixedAllocator::Instance()->Allocate(s);
}
void operator delete( void * p) {
FixedAllocator::Instance()->Deallocate(p);
}
};
struct GSimpl : FastPimpl {
/*use UnixSocketImpl here*/
};
GeneralSocket::GeneralSocket() : pimpl ( new GSimpl){}
GeneralSocket::~GeneralSocket() {
delete pimpl;
pimpl = 0;
}
* This source code was highlighted with Source Code Highlighter .
Inheriting FastPimpl, we can get the closed class we need with a memory allocator of a given size (FixedAllocator).
Let's sum up. When to use Pimpl:
- When it is necessary to separate the abstraction from the implementation. In this case, the necessary implementation can be selected, for example, at the start of the program
- When it is necessary to expand both abstraction and implementation with new classes and arbitrarily combine them. Thus, we obtain two independent class hierarchies of abstraction and implementation
- When the client code should not be recompiled when the implementation changes
- When it is necessary to hide implementation of abstraction from the client
Related Links- Stephen C. Dewhurst. “Slippery C ++. How to avoid problems when designing and compiling your programs. ”(C ++ Gotchas. Avoiding Common Problems in Coding and Design). “Tip 8”
- Coat of arms of Sutter, Andrei Alexandrescu. "C ++ programming standards". “Chapter 43”
- Object-oriented design techniques. Design patterns, E. Gamma, R. Helm, R. Johnson, J. Vlissides. Bridge pattern
- Compilation Firewalls (http://gotw.ca/gotw/024.htm)
- The Fast Pimpl Idiom (http://gotw.ca/gotw/028.htm)