A problem of dependent types

type systems
c++
Published

June 24, 2009

Here’s an interesting problem involving C++ templates. Say you have a class that is parameterized on a term — we’ll use std::bitset<N> as a running example — and you’d like to wrap this class in another class that hides the value of N from its clients.

Of course, it isn’t possible to instantiate a template based on a value that isn’t a compile-time constant, but it may be desirable to choose from one of several (statically-defined) values at run-time. In the std::bitset case, we might define “small,” “medium,” and “large” sizes for a representation class, and have the constructor for wrapper class choose the representation that is large enough to hold the set of bits it is created with. We’d like wrap bitset in such a way so that its size is not an explicit part of the type of the wrapper class (and thus clients can write code that handles wrapped-bitset objects of varying sizes without parameterizing their code). This is a problem, because the size of bitset is part of its type.

In general, this problem manifests whenever a wrapper class can make a run-time decision about which of some static set of template parameter values for its (hidden) representation class is most appropriate; or, still more generally, when it makes sense to choose a different type of representation based on a run-time value. Note that this problem isn’t confined to classes parameterized on terms; a similar problem could also appear with classes parameterized on types, like STL containers. However, it is less immediately clear to me what a solution for classes parameterized on types would look like in the general case, since the type parameter is more likely to be exposed in the container’s interface. (There are special cases, like choosing different types of containers as an internal representation depending on the characteristics of different collections of homogeneous data, that might make sense.)

There are a few approaches to wrapping a class in C++, but none of them are particularly suitable for solving this problem. Here’s why:

  1. Public inheritance does not shield clients from the template value in the representation class, because it must be a template parameter to the wrapper class. However, public inheritance frees the wrapper class implementor from implementing (potentially) many proxy methods that simply call out to to the representation class.
  2. Explicit delegation requires the wrapper class implementor to write proxy methods for each operation of interest from the representation class. However, the template parameter to the representation class is still part of its type. Therefore, there must be several fields in the wrapper class that hold pointers to instances of different instantiations of the representation class template (e.g. std::bitset<32>, std::bitset<128>, and std::bitset<2048>). In addition, each proxy method must include a conditional expression that determines which of these objects is the actual representation and a pointer dereference. (Having the wrapper class include three std::bitset values, rather than pointers, would defeat the purpose of having “small,” “medium,” and “large” representations.)
  3. Private inheritance combines the problems of the first two approaches with the advantages of neither.

A general solution to this problem at the type-system level might involve constrained template parameters and union types. So you could say, e.g., std::bitset<N in {32,128,2048}> b = std::bitset<32>(); This solves our problem by encoding a range of possible sizes in the type of a bitset-valued variable rather than encoding a particular size. (My gut feeling — although I don’t have the turnstiles to prove it — is that this feature might be difficult or impossible to include in a sound, statically-checked type system, let alone to implement in a production compiler.) Sadly, extending the language is rarely a viable option for most software projects.

There are, of course, ways to solve the problem of needing a set of bits large enough to hold k bits, where k isn’t known until runtime: use std::bit_vector or use std::vector<std::bitset<32> >, to give two simple examples. More generally, it would also be possible to use virtual functions in a base representation class that is extended by a templated class: then derived<32> and derived<128> could both be pointed to by a representation*. But I wonder, since I’m continually amazed by feats of template metaprogramming, what clean and efficient solutions to the general problem are possible?