T.gp: Generic programming
Generic programming is programming using types and algorithms parameterized by types, values, and algorithms.
T.1: Use templates to raise the level of abstraction of code
Reason
Generality. Re-use. Efficiency. Encourages consistent definition of user types.
Example, bad
Conceptually, the following requirements are wrong because what we want of T
is more than just the very low-level concepts of "can be incremented" or "can be added":
template<typename T, typename A>
// requires Incrementable<T>
A sum1(vector<T>& v, A s)
{
for (auto x : v) s+=x;
return s;
}
template<typename T, typename A>
// requires Simple_number<T>
A sum2(vector<T>& v, A s)
{
for (auto x : v) s = s + x;
return s;
}
Assuming that Incrementable
does not support +
and Simple_number
does not support +=
, we have overconstrained implementers of sum1
and sum2
.
And, in this case, missed an opportunity for a generalization.
Example
template<typename T, typename A>
// requires Arithmetic<T>
A sum(vector<T>& v, A s)
{
for (auto x : v) s+=x;
return s;
}
Assuming that Arithmetic
requires both +
and +=
, we have constrained the user of sum
to provide a complete arithmetic type.
That is not a minimal requirement, but it gives the implementer of algorithms much needed freedom and ensures that any Arithmetic
type
can be used for a wide variety of algorithms.
For additional generality and reusability, we could also use a more general Container
or Range
concept instead of committing to only one container, vector
.
Note
If we define a template to require exactly the operations required for a single implementation of a single algorithm
(e.g., requiring just +=
rather than also =
and +
) and only those, we have overconstrained maintainers.
We aim to minimize requirements on template arguments, but the absolutely minimal requirements of an implementation is rarely a meaningful concept.
Note
Templates can be used to express essentially everything (they are Turing complete), but the aim of generic programming (as expressed using templates) is to efficiently generalize operations/algorithms over a set of types with similar semantic properties.
Enforcement
- Flag algorithms with "overly simple" requirements, such as direct use of specific operators without a concept.
- Do not flag the definition of the "overly simple" concepts themselves; they may simply be building blocks for more useful concepts.
T.2: Use templates to express algorithms that apply to many argument types
Reason
Generality. Minimizing the amount of source code. Interoperability. Re-use.
Example
That's the foundation of the STL. A single find
algorithm easily works with any kind of input range:
template<typename Iter, typename Val>
// requires Input_iterator<Iter>
// && Equality_comparable<Value_type<Iter>, Val>
Iter find(Iter b, Iter e, Val v)
{
// ...
}
Note
Don't use a template unless you have a realistic need for more than one template argument type. Don't overabstract.
Enforcement
??? tough, probably needs a human
T.3: Use templates to express containers and ranges
Reason
Containers need an element type, and expressing that as a template argument is general, reusable, and type safe. It also avoids brittle or inefficient workarounds. Convention: That's the way the STL does it.
Example
template<typename T>
// requires Regular<T>
class Vector {
// ...
T* elem; // points to sz Ts
int sz;
};
vector<double> v(10);
v[7] = 9.9;
Example, bad
class Container {
// ...
void* elem; // points to size elements of some type
int sz;
};
Container c(10, sizeof(double));
((double*)c.elem)[] = 9.9;
This doesn't directly express the intent of the programmer and hides the structure of the program from the type system and optimizer.
Hiding the void*
behind macros simply obscures the problems and introduces new opportunities for confusion.
Exceptions: If you need an ABI-stable interface, you might have to provide a base implementation and express the (type-safe) template in terms of that. See Stable base.
Enforcement
- Flag uses of
void*
s and casts outside low-level implementation code
T.4: Use templates to express syntax tree manipulation
Reason
???
Example
???
Exceptions: ???
T.5: Combine generic and OO techniques to amplify their strengths, not their costs
Reason
Generic and OO techniques are complementary.
Example
Static helps dynamic: Use static polymorphism to implement dynamically polymorphic interfaces.
class Command {
// pure virtual functions
};
// implementations
template</*...*/>
class ConcreteCommand : public Command {
// implement virtuals
};
Example
Dynamic helps static: Offer a generic, comfortable, statically bound interface, but internally dispatch dynamically, so you offer a uniform object layout. Examples include type erasure as with std::shared_ptr
’s deleter. (But don't overuse type erasure.)
Note
In a class template, nonvirtual functions are only instantiated if they're used -- but virtual functions are instantiated every time. This can bloat code size, and may overconstrain a generic type by instantiating functionality that is never needed. Avoid this, even though the standard facets made this mistake.
Enforcement
- Flag a class template that declares new (non-inherited) virtual functions.