Constrained genericity
You are encouraged to solve this task according to the task description, using any language you may know.
Constrained genericity means that a parametrized type or function (see Parametric Polymorphism) can only be instantiated on types fulfilling some conditions, even if those conditions are not used in that function.
Say a type is called "eatable" if you can call the function eat
on it. Write a generic type FoodBox
which contains a collection of objects of a type given as parameter, but can only be instantiated on eatable types. The FoodBox shall not use the function eat in any way (i.e. without the explicit restriction, it could be instantiated on any type). The specification of a type being eatable should be as generic as possible in your language (i.e. the restrictions on the implementation of eatable types should be as minimal as possible). Also explain the restrictions, if any, on the implementation of eatable types, and show at least one example of an eatable type.
C++
The current C++ standard doesn't support constrained genericity (you can emulate it by having the container refer to the corresponding eat function without actually calling it. The next version will, however, allow it through concepts: <cpp>
- include <concepts>
- include <vector>
auto concept Eatable<typename T> // auto makes it apply automatically {
void eat(T);
};
template<std::Moveable T>
requires Eatable<T>
class FoodBox { public:
std::vector<T> food;
};
</cpp>
The only requirement to implement an Eatable type is, indeed, that a suitable function eat
is defined for it (to put it in the FoodBox, in addition it has to be Moveable, since std::vector
requires that; but that's ortogonal to the type being Eatable). A possible implementation of an eatable type could be:
<cpp>
class Banana {};
void eat(Banana const &) {}
</cpp>
Even a built-in type can be made eatable by defining a suitable eat
function. The following makes double
an eatable type:
<cpp>
void eat(double) {}
</cpp>
Another way to make an existing type eatable is to use a concept map. Let's assume we have an abstract base class Food
which looks like this;
<cpp>
class Food
{
public:
virtual void munch() = 0; virtual ~Food() {}
};
</cpp>
Then we can make all classes derived from Food eatable using Food::munch()
for eat
with the following concept map template:
<cpp>
template<std::DerivedFrom<Food> T>
concept_map Eatable<T>
{
void eat(T const& t) { t->munch(); }
}
</cpp>
The difference to a global function void eat(Food const&)
is that the function in the concept map is only visible to functions using that concept map, thus reducing namespace polution. Functions directly operating on Food
objects can use the interface provided by Food
itself, e.g. apple.munch()
, or explicitly invoke Eatable<Food>::eat(apple)
. Of course, concept maps also work with built-in types:
<cpp>
concept_map Eatable<int>
{
void eat(int) {}
} </cpp>