This exercise will help you practice and demonstrate your ability to perform basic design tasks using multiple forms of polymorphism to address design constraints. Again note that these tasks are not focused on syntax or algorithms but rather on problem solving.
Once again, there are rules.
<algorithms>
,
<numeric>
,
<memory>
,
<optional>
,
<set>
,
<unordered_map>
,
<unordered_set>
,
<queue>
,
<vector>
,
<type_traits>
and
<iostream>
.new
or delete
.using namespace
anywhere.std::any
.Note also that some tasks may be graded qualitatively. For those tasks, producing functioning code alone may not suffice. Instead, you must also show good judgement and design in your code in order to receive full credit. Consider the guidelines we have discussed in class.
A template for the project is available
here.
You should not add new files to the template or they will not be graded.
All of your solutions should modify code in the lib/solutions
directory.
This helps automated checking for adhering to the rules.
Note as well that normal plagiarism detection will be applied. Do not share your code with or show your code to other students.
In this task you will focus on combining multiple forms of polymorphism in order to make a design that provides a clear and clean API to the user. Successfully completing the task will involve combining multiple forms of polymorphism, but identifying where to use different forms can largely be done in an incremental fashion.
You must design a Structure
class. A Structure
is simply a container that
can hold a sequence of heterogeneous elements. This initially sounds trivial,
but the interface of a Structure
should provide for flexible and easy usage.
Achieving those properties adds internal complexity.
In addition, Structure
s should be assignable and copyable while
maintaining ownership over their contents.
A Structure
should support the following operations:
Structure
can be created simply by using the no-args
constructor.Structure
s
, s
can be reassigned via
an assignment operator, e.g. s = Structure{}
;Structure
s
, s
can be copied into other
Structure
s, e.g. t = s
and Structure t = s
.
Each copy of the Structure
should be fully distinct,
and changes to the contents of one structure should not affect the contents
of other structures (both the structure and contents are copied).add
method should accept any type of argument that
(1) is itself both copyable and equality comparable and
(2) separately has a printElement
overload for the corresponding type.
A compiler error should result from cases where the argument is not copyable,
but there are no constraints on the nature of this compiler error.
The argument to the add
method is then copied to the
end of the Structure
.print
method should take an output stream,
e.g. std::cout
, and call printElement
for each item in the Structure
in
order.
The printElement
function for a given type will take two arguments,
an output stream (std::ostream
) and the element to print.
You can find examples of these within the unit tests.
Observe that printElement
is an overloaded function must be resolved via the
static type of its arguments.
A newline should be printed to the given stream after each call to
printElement
.Structure
s can be compared via the ==
operator. If their contents are equal by value comparison of their individual
elements, then the operator returns true.
Empty Structure
s are trivially equal.
Otherwise two Structure
s are nonequal.
Surprisingly, this is the most difficult challenge in the task.
I think that doing it without dynamic casting is the hardest part of the exercise.
Consider it a stretch goal and come back to it if you have time.You will implement the Structure
class. You can provide and declarations
required in task01.h
and you may place additional code in task01.cpp
.
You will also implement a printElement
function overload for Structure
s
themselves.
Note: This means that Structure
s can contain Structure
s, and the entire
construct is hierarchical. This should simply use the print
method of the
passed in Structure
.
Your solution must be able to support adding any element that is assignable,
copyable, and has a printElement
overload, even if it is not present in the
provided code. In other words, you code should again be extensible without
modification. It should be open and closed.
You will find tests for this task in the unit01-*.cpp
unit tests and
corresponding binaries.
Additional tests that add new types to the Structure
may be used during
grading.
Consider: This design hard wires a single printing behavior into the structure. Can we make that behavior itself customizable within this design? If so, how? If not, why not?
In exercise 5, you explored implementing Dijkstra's shortest paths algorithm using inheritance to provide an interface that allows for "programs with holes". Now, you will explore the same problem again, but you are not allowed to use inheritance within your solution. You may feel free to revisit your previous code and tweak it to the new use case. Observe that you must still achieve "programs with holes", and because your solution must be able to work with different types of graphs, this means you will be using polymorphism. Recall that we discussed four forms of polymorphism in class. We saw one in particular that can provide flexible, efficient solutions to this problem. Specifically, approaches based on traits, analogous to type classes, provide a good direction.
Instead of working on a concrete Graph
type, your shortest paths algorithm
will be able to simply accept different types of graphs as if the implementation
were overloaded for them.
Nonetheless, you should only implement Dijkstra's algorithm once and reuse it
for each graph kind.
By defining a type trait that captures the behaviors you require, the algorithm
should work transparently.
It can also be extended to entirely new types of graphs without modifying
any of the code you write in task02.h
.
Note that type traits require defining a specialization in order to extend support to a new type of graph. You will need to support the same two types of graphs considered in exercise 5 by defining specializations for them.
Define a function called shortestPath
in task02.h
that returns the list
of nodes in the shortest path from a given source node to a given target.
When the source and target are the same, the list should contain only one copy
of that node itself.
When there is no path from the source to the target, the list should be empty.
Notice that you may not know all of the nodes of the graph in advance.
NOTE: In contrast to the function from exercise 5, this one should take three
arguments. The first will be the type representing the graph.
The second should be the source node.
The third should be the target node.
You must use type traits to support a generic interface. You can find the
two kinds of graphs from exercise 5 again in Support.h
.
You should define specializations for these graphs to enable your shortestPath
function to work transparently when provided appropriate arguments.
You can work from the provided tests to clarify examples and behavior.
You may also be particularly interested in using the weight calculations in
unit02.cpp
of exercise 5.
Create an archive of your solution by changing into the directory containing your project template and running:
You can then upload this archive to CourSys.