Hi there.
Someone on the chat today was asking about the pimpl idiom, and I thought I'd write up a quick little example. The purpose of an opaque pointer is to move the the implementation specific dependencies (types, macros, includes, and et. al.) away from the header and into the actual source where its used. Including "Foobar.h" from "Unrelated.cpp" also includes iostream, string, vector, windows.h (namespace polluting!), and the entirety of "expensive third party library". If "Foobar" used an opaque pointer, then "Unrelated.cpp" can use a Foobar, without having to know about "string", "vector", "windows.h", or the third party library.
Let's say, as a contrived example, you wanted to wrap std::vector. Such a class might look something like:
// simple_list.h
#include <vector>
#include <string>
class simple_list {
public:
void add(const std::string& str);
void dump() const;
private:
std::vector<std::string> _data;
};
// simple_list.cpp
#include "simple_list.h"
void simple_list::add(const std::string& str) {
_data.push_back(str);
}
Notice how including "simple_list.h" will also include "vector" and "string".
An opaque pointer will hide the details of its implementation:
// simple_list.h
class simple_list {
public:
simple_list();
~simple_list();
simple_list(const simple_list&) = delete;
simple_list& operator= (const simple_list&) = delete;
public:
void add(const char* str);
void dump() const;
private:
struct impl;
impl* _impl;
};
// simple_list.cpp
#include "simple_list.h"
#include <vector>
#include <string>
struct simple_list::impl {
std::vector<std::string> data;
};
simple_list::simple_list() {
_impl = new impl;
}
simple_list::~simple_list() {
delete impl;
}
simple_list::add(const char* str) {
_impl->data->push_back(str);
}
Note how simple_list.h no longer requires the "vector" or "string" headers, and how the implementation structure is well contained in simple_list.cpp. Unrelated.cpp can use a "simple_list" without first knowing about "vector" or "string".
Also note how simple_list now has a dynamic allocation with it. This is one of the main disadvantages of an opaque pointer, but it can be avoided.
This is my favorite approach to the opaque pointer idiom: Just reserve some space for the implementation:
// simple_list.h
#pragma once
class simple_list {
public: // noncopyable
simple_list();
~simple_list();
simple_list(const simple_list&) = delete;
simple_list& operator= ( const simple_list& ) = delete;
public: // interface
void add(const char* str);
void dump() const;
private: // pimpl
struct internal_impl;
internal_impl& impl();
const internal_impl& impl() const;
private: // data
int _data[6];
};
// simple_list.cpp
#include "simple_list.h"
#include <vector>
#include <iterator>
#include <iostream>
#include <string>
struct simple_list::internal_impl {
internal_impl() {
}
~internal_impl() {
}
std::vector<std::string> _data;
};
simple_list::simple_list() {
static_assert(sizeof(internal_impl) < sizeof(_data), "simple_list::_data too small for internal_impl");
new (_data) internal_impl;
}
simple_list::~simple_list() {
impl().~internal_impl();
}
void simple_list::add(const char* str) {
impl()._data.push_back(str);
}
void simple_list::dump() const {
using namespace std;
const auto& vec = impl()._data;
cout << "{";
if ( vec.size() ) {
copy(vec.begin(), vec.end()-1, ostream_iterator<string>(cout, ", "));
cout << vec.back();
}
cout << "}";
}
simple_list::internal_impl& simple_list::impl() {
return *reinterpret_cast<internal_impl*>(_data);
}
const simple_list::internal_impl& simple_list::impl() const {
return *reinterpret_cast<const internal_impl*>(_data);
}
// main.cpp
#include "simple_list.h"
int main(int, char*[]) {
simple_list f;
f.add("the");
f.add("quick");
f.add("brown");
f.add("fox");
f.dump();
}
The simple_list now reserves 6*sizeof(int) bytes for the implementation, backed by a static_assert on the constructor to make sure the buffer doesn't overrun. Memory alignment and cache lines considered, this might be an acceptable compromise: Trade off the double indirection with a possibility of some wasted space.
Though, the point is moot, because in this contrived example, the vector and all its strings will store its data on the heap anyways.
That's all for now. See you around.
nice article