• entries
    18
  • comments
    34
  • views
    28833

Quick Opaque Pointer Example

Sign in to follow this  

2416 views

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 #include class simple_list {public: void add(const std::string& str); void dump() const;private: std::vector _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.hclass 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 #include struct simple_list::impl { std::vector 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 onceclass 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 #include #include #include struct simple_list::internal_impl { internal_impl() {} ~internal_impl() {} std::vector _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(cout, ", ")); cout << vec.back(); } cout << '}';}simple_list::internal_impl& simple_list::impl() { return *reinterpret_cast(_data);}const simple_list::internal_impl& simple_list::impl() const { return *reinterpret_cast(_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.

Sign in to follow this  


4 Comments


Recommended Comments

I'm not really happy with that. Granted, you static_assert the size but you should also really static_assert the alignment. Personally I also dislike doing that work in your concrete class. Right now, adding a constructor has a relatively high risk of forgetting to initialize the opaque pointer.

I have lately been toying around with something like this:
template <typename T, std::size_t Length, std::size_t Align = alignof(void*)> class opaque_data
{
public:
	/**
	 * The underlying type.
	 */
	using type = T;

	/**
	 * Initializes the underlying data.
	 * @param args any arguments to be passed to the underlying type's
	 * constructor.
	 */
	template <typename ... Args> opaque_data(Args&&... args)
	{
		static_assert(sizeof(type)  <= Length, "not enough storage available");
		static_assert(alignof(type) <= Align , "alignment requirements not met");

		new (reinterpret_cast<type*>(&storage)) type{std::forward<Args>(args)...};
	}
	/**
	 * Initializes the underlying data using a subclass.
	 * @param args any arguments to be passed to the subclass' constructor.
	 */
	template <typename S, typename ... Args> opaque_data(tag<S>, Args&&... args)
	{
		static_assert(sizeof(S)  <= Length                    , "not enough storage available");
		static_assert(alignof(S) <= Align                     , "alignment requirements not met");
		static_assert(std::is_base_of<type, S>::value         , "the specified type is not a subclass of the underlying type");
		static_assert(std::has_virtual_destructor<type>::value, "inheritance is used and base underlying type has no virtual destructor");

		new (reinterpret_cast<S*>(&storage)) S(std::forward<Args>(args)...);
	}
	/**
	 * Destroys the underlying data.
	 */
	~opaque_data()
	{
		reinterpret_cast<type*>(&storage)->~type();
	}

	/**
	 * Returns a reference to the underlying data.
	 * @return a reference to the underlying data.
	 */
	type& data()
	{
		return *reinterpret_cast<type*>(&storage);
	}
	/**
	 * Returns a reference to the underlying data.
	 * @return a reference to the underlying data.
	 */
	type const& data() const
	{
		return *reinterpret_cast<type const*>(&storage);
	}

	/**
	 * Swaps the underlying data with another instance.
	 * @param other another instance.
	 */
	void swap(opaque_data& other)
	{
		using std::swap;
		swap(data(), other.data());
	}

	/**
	 * Allows access to the underlying type's data members.
	 * @return a pointer to the underlying type.
	 */
	type* operator -> ()
	{
		return reinterpret_cast<type*>(&storage);
	}
	/**
	 * Allows access to the underlying type's data members.
	 * @return a pointer to the underlying type.
	 */
	type const* operator -> () const
	{
		return reinterpret_cast<type const*>(&storage);
	}

	/**
	 * Returns a reference to the underlying data.
	 * @return a reference to the underlying data.
	 */
	type& operator * ()
	{
		return data();
	}
	/**
	 * Returns a reference to the underlying data.
	 * @return a reference to the underlying data.
	 */
	type const& operator * () const
	{
		return data();
	}

	/// @name Deleted Constructors and Operators
	//@{
	opaque_data(opaque_data const&) = delete;
	opaque_data(opaque_data&&) = delete;
	opaque_data& operator = (opaque_data const&) = delete;
	opaque_data& operator = (opaque_data&&) = delete;
	//@}

	/**
	 * Swaps the underlying data of two instances.
	 * @param lhs the first participant of the swap.
	 * @param rhs the second participant of the swap.
	 */
	friend void swap(opaque_data& lhs, opaque_data& rhs)
	{
		lhs.swap(rhs);
	}

private:
	/**
	 * The storage for the underlying data.
	 */
	std::aligned_storage_t<Length, Align> storage;
};

Share this comment


Link to comment
BitMaster: Perfect!

I'll admit that my approach isn't perfect or the best, but again, it is just a quick example...

Share this comment


Link to comment
Well, the only thing that really bugged me was the missing alignment check. Mostly because if not for that, I would have used one of the types I used with a bad alignment myself.

And then I thought: why not take the opportunity to add your own attempt to the discussion, things like that can always use an extra pair of eyes to look through it. ;)

Share this comment


Link to comment

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now