Stannum

True encapsulation in C++

2019-04-25 Permalink

Access specifiers are taught as the encapsulation mechanism in C++. However, I’ve mentioned before how poorly they perform their function.

For example:

class A {
public:
	// ... public interface ...
private:
	void f(C);
	B b;
};

In order for this code to compile, B must be previously defined and C must be previously declared. Both of these types, as well as the function f and the layout of the private members of A are an implementation detail. Yet they leak out to the users of A.[1] This in turn has negative impact on compile times (of incremental builds in particular), makes headers harder to comprehend,[2] and makes it harder to change A implementation without breaking the ABI.

In the following text I refer to this approach as the ‘traditional’ method. I dwell deeper into alternative approaches to encapsulation in C++ and present their pros and cons. For the purpose of comparing various encapsulation mechanisms, I evaluate them based on the following criteria:

I’m trying to address here only well-contained and well-encapsulated software components, potentially dynamically linked ones. Therefore it’s assumed that A has no template or inline functions and no compiler generated constructors or operators. These features require the innards of the class to be accessible at compile time by design,[3] thus contradictory to our encapsulation efforts.

Friendly statics

Moving the private functions out of the class definition is easy. One just needs to put them in a friendly class of A:

class A {
public:
	// ... public interface ...
private:
	frind class A_impl;
	B b;
};

And in the translation unit implementing A:

struct A_impl {
	static void f(A &that, C c) {
		// ... work on that and c ...
	}
};

A::A() {
	A_impl::f(*this, ...);
}

This hides the private member function declarations as well as all the types they depend on, at a cost of some verbosity.

Pimpl idiom

The next improvement is to hide the private data members. One frequently used way of doing that is the so-called ‘pimpl idiom’. The private data members are extracted to a separate struct visible only within the translation units implementing our class. The public interface works with the data in that struct. Applying it to our example:

class A {
public:
	// ... public interface ...
private:
	friend struct A_impl;
	std::unique_ptr<A_impl> _;
};

And in the translation unit:

struct A_impl {
	B b;
	static void f(A &that, C c) { ... }
};

A::A() : _(new A_impl) {
	A_impl::f(*this, ...);
}

This entirely hides the implementation details of A, and makes the ABI immune to changes in A’s implementation.

Unfortunately the downside of this approach is the incurred memory fragmentation: if A’s functionality is extended and each class in the hierarchy uses the same methodology to hide its own private members:

class AA : public A {
public:
	// ... public interface ...
private:
	friend struct AA_impl;
	std::unique_ptr<AA_impl> _;
};

And then an array of AAs is allocated:

AA a[10];

Then each instance of AA ends up requiring two dynamic memory allocations, 20 in total, whereas the original code required none.[4]

It’s worthy to note that pimpl is in essence similar to the way C libraries return opaque pointers to their internally allocated structures. For example, SQLite has a function to allocate and initialize an sqlite3 object whose internals are hidden:

int sqlite3_open(const char *filename, sqlite3 **ppDb);

and the rest of the functions operate on that opaque sqlite3 pointer:

int sqlite3_db_config(sqlite3*, int op, ...);

The only difference is that such C code can have multiple copies of the sqlite3 pointer laying around, each operating on the connection directly, whereas the C++ pimpl code would require an extra indirection for each such operation.

Polymorphism

Another frequently used method is to hide the implementation behind an abstract interface. A factory function is provided to create an instance of the concrete implementation:

class A {
public:
	// ... public interface ... (everything is pure virtual)
};
A *make_A();

And in the translation unit:

class A_impl : public A {
	// ... implementation ...
	void f(C c) { ... }
	B b;
};
A *make_A() { return new A_impl; }

This has slightly different trade-offs compared to pimpl:

An array of As would now be defined like

A *a[10];

And populating it would require one memory allocation per instance, even though it’s technically homogeneous.

Opaque char[]

This is the least used method.[5]

Since it was assumed that A has no inline or template functions, the only thing that the compiler cares about when creating instances of A is the size of the memory required and its alignment. Realizing this one can tell the compiler exactly those two pieces of information:

class A {
public:
	// ... public interface ...

	alignas(void*) char _[sizeof(void*)*8]; // reserved for private use
};

And in the translation unit:

struct A_impl {
	B b;
};
static_assert(sizeof(A_impl) <= sizeof(A::_), "reserved memory is too small");

static void f(A &that, C c) {
	A_impl *p = (A_impl*)&that._[0];
	// ... work on that, p and c ...
}

A::A() {
	new(_) A_impl; // initialize private data
	f(*this, ...);
}

Comparing to C again, this is analogous to how C structures have reserved or internal fields in their publicly exposed structures (e.g. see OVERLAPPED structure of WinAPI).

Sure, this method is the most verbose and the least ‘C++ style’. There’s also a risk that, if used thoroughly, too much memory would be reserved and unused. Nonetheless, it checks off each of the criteria set above:

At this point, a natural question to ask is—“why use classes at all then”?

Because a C++ programmer would still benefit from a lot of built-in C++ machinery, including constructors, destructors, virtual tables, RTTI and casts. They all are tricky to do otherwise (even though C programmers would disagree).

Conclusion

Thereby I presented a number of alternatives to the ‘traditional’ encapsulation method. They all improve upon the encapsulation provided by access-specifiers at a cost of verbosity. This raises the question of whether access-specifiers are at all needed in the language?

A Better Language™ would have a way to express the equivalent of the above mechanisms in an easier way. For example it could be useful if the size of the required memory could be pulled during link-time. Another direction of research is to decouple object layouts from compile time features like inheritance or destructors. Describing arbitrary object layouts would facilitate data-oriented code.

To conclude: being a close-to-the-metal language, C++ gives us the tools to do anything, albeit the better solutions aren’t necessarily the prettiest ones.

Footnotes

  1. C++ headers are, in a sense, interfaces.
  2. C++ headers frequently serve a substitute for documentation.
  3. The memory of the array is assumed to be managed by the user of the class. E.g. it can be statically allocated or pooled.
  4. As far as C++ libraries go, I’ve seen it used only in pugixml.

Share on