Date: | 2020-07-14 |
- 1 Change Log
- 2 The Task
- 2.1 Definitions [gdwg.definitions]
- 2.2 Constructors [gdwg.ctor]
- 2.3 Modifiers [gdwg.modifiers]
- 2.4 Accessors [gdwg.accessors]
- 2.5 Range access [gdwg.range.access]
- 2.6 Comparisons [gdwg.cmp]
- 2.7 Extractor [gdwg.io]
- 2.8 Iterator [gdwg.iterator]
- 2.9 Compulsory internal representation [gdwg.internal]
- 2.10 Other notes [other.notes]
- 3 Getting Started
- 4 Marking Criteria
- 5 Originality of Work
- 6 Submission
- 7 Late Submission Policy
- 8 CMake Tools workaround
Please git pull
frequently to get the latest changes.
You will be required to update the toolchain for this assignment. The CSE machines have already taken this update into account. See Tutorial 8 for directions on how to update at home.
- 2020-07-22:
- Removes mandatory
noexcept
specifier fromis_connected
,is_node
,nodes
, andfind
(you can still add them if you think they're necessary). - Removes confusing "diamond suffix" from
operator<<
- Clarifies complexity for range-based
erase_edge
, to make it clearer that the complexity is with respect to the range passed in (and the graph itself is just a really big constant). - Changes exception messages for
replace_node
andconnections
so that they match the member functions' names.Member function Before After replace_node
"Cannot call comp6771::graph<N, E>::replace on a node that doesn't exist"
"Cannot call comp6771::graph<N, E>::replace_node on a node that doesn't exist"
connections
"Cannot call gdwg::graph<N, E>::connected if src doesn't exist in the graph"
"Cannot call gdwg::graph<N, E>::connections if src doesn't exist in the graph"
- Removes mandatory
- 2020-07-20: Updates complexity for
erase_edge
and updatescmake-kits.json
to work around vcpkg bug. - 2020-07-19: Updates complexity for
erase_node
anderase_edges
- 2020-07-18: Implements workaround for CMake Tools configure bug
- First revision
Write a graph
library type in C++, in include/gdwg/graph.hpp
.
In this assignment, you will write a generic directed weighted graph (GDWG) with value-semantics in C++. Both the data stored at a node and the weight stored at an edge will be parameterised types. The types may be different. For example, here is a graph with nodes storing std::string
and edges weighted by int
:
Formally, this directed weighted graph G = (N, E) will consist of a set of nodes N and a set of weighted edges E.
All nodes are unique, that is to say, no two nodes will have the same value and shall not compare equal using operator==
.
Given a node, an edge directed into it is called an incoming edge and an edge directed out of it is called an outgoing edge. The in-degree of a node is the number of its incoming edges. Similarly, the out-degree of a node is the number of its outgoing edges. Given a directed edge from src
to dst
, src
is the source node and dst
is known as the destination node.
Edges can be reflexive, that is to say, the source and destination nodes of an edge could be the same.
G is a multi-edged graph, as there may be two edges from the same source node to the same destination node with two different weights. Two edges from the same source node to the same destination node cannot have the same weight.
Some words have special meaning in this document. This section precisicely defines those words.
- Preconditions: the conditions that the function assumes to hold whenever it is called; violation of any preconditions results in undefined
- Effects: the actions performed by the function.
- Postconditions: the conditions (sometimes termed observable results) established by the function.
- Returns: a description of the value(s) returned by the function.
- Throws: any exceptions thrown by the function, and the conditions that would cause the exception.
- Complexity: the time and/or space complexity of the function.
- Remarks: additional semantic constraints on the function.
- Unspecified: the implementation is allowed to make its own decisions regarding what is unspecified, provided that it still follows the explicitly specified wording.
An Effects element may specify semantics for a function
F
in code using the term Equivalent to. The semantics forF
are interpreted as follows:- All of the above terminology applies to the provided code, whether or not it is explicitly specified. [Example: If
F
has a Preconditions element, but the code block doesn’t explicitly check them, then it is implied that the preconditions have been checked. —end example] - If there is not a Returns element, and
F
has a non-void
return type, all the return statements are in the code block. - Throws, Postconditions, and Complexity elements always have priority over the code block.
- All of the above terminology applies to the provided code, whether or not it is explicitly specified. [Example: If
Specified complexity requirements are upper bounds, and implementations that provide better complexity guarantees meet the requirements.
The class synopsis is the minimum text your header requires to compile most tests (this doesn’t mean that it will necessarily link or run as expected).
Blue text in code will link to C++ Reference or to another part of this document.
This section makes use of [stable.names]. A stable name is a short name for a (sub)section, and isn’t supposed to change. We will use these to reference specific sections of the document. [Example:
Student: Do we need to define
gdwg::graph<N, E>::operator!=
?Tutor: [other.notes] mentions that you don’t need to so you can get used to C++20’s generated operators.
—end example]
It’s very important your constructors work. If we can’t validly construct your objects, we can’t test any of your other functions.
Effects: Value initialises all members.
Throws: Nothing.
- Effects: Equivalent to:
graph(il.begin(), il.end());
- Effects: Initialises the graph’s node collection with the range
[first, last)
.
- Effects: Initialises the graph’s node and edge collections with the range
[first, last)
.
- Postconditions:
*this
is equal to the valueother
had before this constructor’s invocation.other.empty()
istrue
.- All iterators pointing to elements owned by
*this
prior to this constructor’s invocation are invalidated. - All iterators pointing to elements owned by
other
prior to this constructor’s invocation remain valid, but now point to the elements owned by*this
.
- Effects: All existing nodes and edges are either move-assigned to, or are destroyed.
- Postconditions:
*this
is equal to the valueother
had before this operator’s invocation.other.empty()
istrue
.- All iterators pointing to elements owned by
*this
prior to this operator’s invocation are invalidated. - All iterators pointing to elements owned by
other
prior to this operator’s invocation remain valid, but now point to the elements owned by*this
.
- Returns:
*this
.
- Postconditions:
*this == other
istrue
.
Postconditions:
*this == other
istrue
.- All iterators pointing to elements owned by
*this
prior to this operator’s invocation are invalidated.
Returns:
*this
.
Effects: Adds a new node with value
value
to the graph if, and only if, there is no node equivalent tovalue
already stored.Postconditions: All iterators are invalidated.
Returns:
true
if the node is added to the graph andfalse
otherwise.
Effects: Adds a new edge representing
src
→dst
with weightweight
, if, and only if, there is no edge equivalent tovalue_type{src, dst, weight}
already stored. [Note: Nodes are allowed to be connected to themselves. —end note]Postconditions: All iterators are invalidated.
Returns:
true
if the node is added to the graph andfalse
otherwise.Throws:
std::runtime_error("Cannot call gdwg::graph<N, E>::insert_edge when either src or dst node does not exist")
if either ofis_node(src)
oris_node(dst)
arefalse
.
Effects: Replaces the original data,
old_data
, stored at this particular node by the replacement data,new_data
. Does nothing ifnew_data
already exists as a node.Postconditions: All iterators are invalidated.
Returns:
false
if a node that contains valuenew_data
already exists andtrue
otherwise.Throws:
std::runtime_error("Cannot call comp6771::graph<N, E>::replace_node on a node that doesn't exist")
ifis_node(old_data)
isfalse
.
Effects: The node equivalent to
old_data
in the graph are replaced with instances ofnew_data
. After completing, every incoming and outgoing edge ofold_data
becomes an incoming/ougoing edge ofnew_data
, except that duplicate edges shall be removed.Postconditions: All iterators are invalidated.
Throws:
std::runtime_error("Cannot call comp6771::graph<N, E>::merge_replace_node on old or new data if they don't exist in the graph")
if either ofis_node(old_data)
oris_node(new_data)
arefalse
. [Note: You are not required to replaceold
ornew data
with the values ofold_data
andnew_data
, unlike in Assignment 2 —end note][Note: The following examples use the format (Nsrc, Ndst, E). [Example: Basic example.
- Operation:
merge_replace_node(A, B)
- Graph before: (A, B, 1), (A, C, 2), (A, D, 3)
- Graph after : (B, B, 1), (B, C, 2), (B, D, 3)
—end example][Example: Duplicate edge removed example.
- Operation:
merge_replace_node(A, B)
- Graph before: (A, B, 1), (A, C, 2), (A, D, 3), (B, B, 1)
- Graph after : (B, B, 1), (B, C, 2), (B, D, 3)
—end example][Example: Diagrammatic example.
—end example] —end note]
Effects: Erases all nodes equivalent to
value
, including all incoming and outgoing edges.Complexity: O(log(n) + e), where n is the total number of stored nodes and e is the total number of stored edges.
Returns:
true
ifvalue
was removed;false
otherwise.
Effects: Erases an edge representing
src
→dst
with weightweight
.Returns:
true
if an edge was removed;false
otherwise.Postconditions: All iterators are invalidated.
Throws:
std::runtime_error("Cannot call comp6771::graph<N, E>::erase_edge on src or dst if they don't exist in the graph")
if eitheris_node(src)
oris_node(dst)
isfalse
.Complexity: O(log(n) + e), where n is the total number of stored nodes and e is the total number of stored edges.
Effects: Erases the edge pointed to by
i
.Complexity: Amortised constant time.
Returns: An iterator pointing to the element immediately after
i
prior to the element being erased. If no such element exists, returnsend()
.
Effects: Erases all edges in the range
[i, s)
.Complexity O(d), where d=
ranges::distance(i, s)
.Returns: An iterator equivalent to
s
prior to the range being erased. If no such element exists, returnsend()
.
Effects: Erases all nodes from the graph.
Postconditions:
empty()
istrue
.
Returns:
true
if a node equivalent tovalue
exists in the graph, andfalse
otherwise.Complexity: O(log (n)) time.
- Returns:
true
if there are no nodes in the graph, andfalse
otherwise.
Returns:
true
if the edgesrc
→dst
exists in the graph, andfalse
otherwise.Throws:
std::runtime_error("Cannot call gdwg::graph<N, E>::is_connected if src or dst node don't exist in the graph")
if either ofis_node(src)
oris_node(dst)
arefalse
.
Returns: A sequence of all stored nodes, sorted in ascending order.
Complexity: O(n), where n is the number of stored nodes.
Returns: A sequence of weights from
src
todst
, sorted in ascending order.Complexity: O(log (n) + e), where n is the number of stored nodes and e is the number of stored edges.
Throws:
std::runtime_error("Cannot call gdwg::graph<N, E>::weights if src or dst node don't exist in the graph")
if either ofis_node(src)
oris_node(dst)
arefalse
.
Returns: An iterator pointing to an edge equivalent to
value_type{src, dst, weight}
, orend()
if no such edge exists.Complexity: O(log (n) + log (e)), where n is the number of stored nodes and e is the number of stored edges.
Returns: A sequence of nodes (found from any immediate outgoing edge) connected to
src
, sorted in ascending order, with respect to the connected nodes.Complexity: O(log (n) + e), where e is the number of outgoing edges associated with
src
.Throws:
std::runtime_error("Cannot call gdwg::graph<N, E>::connections if src doesn't exist in the graph")
ifis_node(src)
isfalse
.
- Returns: An iterator pointing to the first element in the container.
Returns: An iterator denoting the end of the range that
begin()
points to.Remarks:
[begin(), end())
shall denote a valid range.
Returns:
true
if*this
andother
contain exactly the same nodes and edges, andfalse
otherwise.Complexity: O(n + e) where n is the sum of stored nodes in
*this
andother
, and e is the sum of stored edges in*this
andother
.
Effects: Behaves as a formatted output function of
os
.Returns:
os
.Remarks: The format is specified thusly:
[source_node1]
, …, [source_noden]
are placeholders for all nodes that the graph stores, sorted in ascending order. [edges1]
, …, [edgesn]
are placeholders for
where [noden_conencted_node1] | [weight]
, …, [noden_connected_noden] | [weight]
are placeholders for each node’s connections and corresponding weight, also sorted in ascending order. [Note: If a node doesn’t have any connections, then its corresponding [edgesn]
should be a line-separated pair of parentheses —end note]
[Example:
using graph = gdwg::graph<int, int>;
auto const v = std::vector<graph::value_type>{
{4, 1, -4},
{3, 2, 2},
{2, 4, 2},
{2, 1, 1},
{6, 2, 5},
{6, 3, 10},
{1, 5, -1},
{3, 6, -8},
{4, 5, 3},
{5, 2, 7},
};
auto g = graph(v.begin(), v.end());
g.insert_node(64);
auto out = std::ostringstream{};
out << g;
auto const expected_output = std::string_view(R"(1 (
5 | -1
)
2 (
1 | 1
4 | 2
)
3 (
2 | 2
6 | -8
)
4 (
1 | -4
5 | 3
)
5 (
2 | 7
)
6 (
2 | 5
3 | 10
)
64 (
)
)");
CHECK(out.str() == expected_output);
—end example ]
template<concepts::regular N, concepts::regular E>
requires concepts::totally_ordered<N>
class graph<N, E>::iterator {
public:
using value_type = ranges::common_tuple<N, N, E>;
using difference_type = std::ptrdiff_t;
using iterator_category = std::bidirectional_iterator_tag;
// Iterator constructor
iterator() = default;
// Iterator source
auto operator*() -> ranges::common_tuple<N const&, N const&, E const&>;
// Iterator traversal
auto operator++() -> iterator&;
auto operator++(int) -> iterator;
auto operator--() -> iterator&;
auto operator--(int) -> iterator;
// Iterator comparison
auto operator==(iterator const& other) -> bool;
private:
explicit iterator(unspecified);
};
Elements are lexicographically ordered by their source node, destination node, and edge weight, in ascending order.
Nodes without any connections are not traversed.
[Note:
gdwg::graph<N, E>::iterator
modelsranges::bidirectional_iterator
. —end note]
Effects: Value-initialises all members.
Remarks: Pursuant to the requirements of
std::forward_iterator
, two value-initialised iterators shall compare equal.
Effects: Constructs an iterator to a specific element in the graph.
Remarks: There may be multiple constructors with a non-zero number of parameters.
- Effects: Equivalent to:
Remarks:
ranges::common_tuple
is almost completely interface-compatible withstd::tuple
, but has subtle differences that make this assignment possible.[Note: Do not use
ranges::make_common_tuple
orstd::make_tuple
, as these will always copy values instead of returning references. —end note]
- Effects: Advances
*this
to the next element in the range.
[Example: In this way, your iterator will iterator through a graph like the one below producing the following tuple values when deferenced each time:
—end example]
- Returns:
*this
.
- Effects: Equivalent to:
Effects: Advances
*this
to the previous element in the range.Returns:
*this
.
- Effects: Equivalent to:
- Returns:
true
if*this
andother
are pointing to elements in the same range, andfalse
otherwise.
Your graph is required to own the resources (nodes and edges) that are passed in through the insert functions. This means creating memory on the heap and doing a proper copy of the values from the caller. This is because resources in your graph should outlive the caller’s resouce that was passed in in case it goes out of scope. For example, we want the following code to be valid.
Your graph is required to use smart pointers (however you please) to solve this problem.
For each edge, you are only allowed to have one underlying resource (heap) stored in your graph for it.
For each node, you are only allowed to have one underlying resource (heap) stored in your graph for it.
[Hint: In your own implementation you’re likely to use some containers to store things, and depending on your implementation choice, somewhere in those containers you’ll likely use either
std::unique_ptr<N>
orstd::shared_ptr<N>
—end hint]
You could feasibly implement the assignment without any smart pointers, through lots of redundant copying. For example, having a massive data structure like:
You can see that in this structure you would have duplicates of nodes when trying to represent this complex structure. This takes up a lot of space. We want you to build a space efficient graph. This means only storing one instance of each node and edge.
You must:
- Include a header guard in
include/gdwg/graph.hpp
- Use C++20 style and methods where appropriate
- Make sure that all appropriate member functions are
const
-qualified - Leave a moved-from object in a state with no nodes.
- Implement this class within the namespace
gdwg
. - Assignment 2 asked you to implement
operator!=
because you’ll see it in a lot of production codebases, and it’s important that you know how to write it correctly. To get used to how C++20 handlesoperator!=
, we’re asking that you do not implement an overload foroperator!=
in Assignment 3. - Write a Markdown document
test/README.md
explaining your rationale behind the decisions you made while testing.
You must not:
- Write to any files that aren’t provided in the repo (e.g. storing your vector data in an auxilliary file)
- Add additional members to the public interface.
You:
- Should try to mark member functions that will not throw exceptions with
noexcept
- Are not required to make any member function explicit unless directly asked to in the spec.
We have deliberately removed the const
-qualifiers for most member functions from the specification. You are required to work out which functions must be const
-qualified. You must ensure that each operator and member function appropriately either has:
- A
const
member function; or - A non-
const
member function; or - Both a
const
and non-const member function
Please think carefully about this. The function declarations intentionally do not specify their constness, except for the begin()
and end()
member functions. These are const
-qualified to help you out.
In most cases you will only need a single function in the overload set.
If you haven’t done so already, clone this repository.
(Note: Replace z5555555 with your zid)
Navigate inside the directory. You can then open vscode with code .
(note the dot).
Similar to the first tutorial, you simply to have to run Ctrl+Shift+P
and then type Run Test
and hit enter. VS Code will compile and run all of your tests and produce an output.
Part of your assignment mark will come from the quality and extensiveness of tests that you write.
You can add more test files to the test/graph/
directory. Simply copy test/graph/graph_test1.cpp
into another file in that directory.
Note, everytime that you add a new file to the test/graph/
directory you will need to add another few lines to test/CMakeLists.txt
. You can once again, simply copy the test reference for graph_test1.cpp
and rename the appropriate parts. Every time you update CMakeLists.txt
in any repository, in VSCode you should codess Ctrl+Shift+P
and run Reload Window
for the changes to take effect.
This assignment will contribute 20% to your final mark.
The assessment for the assignment recognizes the difficulty of the task, the importance of style, and the importance of appropriate use of programming methods (e.g. using while loops instead of a dozen if statements).
50% |
Correctness The correctness of your program will be determined automatically by tests that we will run against your program. You will not know the full sample of tests used prior to marking. |
25% |
Your tests You are required to write your own tests to ensure your program works. You will write tests in the test/ directory. Write a Markdown document test/README.md explaining your rationale behind the decisions you made while testing. Please read the Catch2 tutorial or review lecture/tutorial content to see how to write tests. Tests will be marked on several factors. These include, but are not limited to:
|
20% |
C++ best practices Your adherence to good C++ best practice in lecture. This is not saying that if you conform to the style guide you will receive full marks for this section. This 20% is also based on how well you use modern C++ methodologies taught in this course as opposed to using backwards-compatible C methods. Examples include: Not using primitive arrays and not using pointers. We will also penalise you for standard poor practices in programming, such as having too many nested loops, poor variable naming, etc. |
5% |
clang-format In your project folder, run the following commands on all cpp/h files in the source and test directory.$ clang-format-11 -style=file -i /path/to/file.cpp We will run clang-format-11 -style=file /path/to/file.cpp | diff /path/to/file.cpp - on all your headers and source files. If, for each of these files, diff outputs nothing (i.e. is formatted correctly), you will receive full marks for this section (5%). A video explaining how to use clang-format can be found HERE.
|
The following actions will result in a 0/100 mark for this assignment, and in some cases a 0 for COMP6771:
- Knowingly providing your work to anyone and it is subsequently submitted (by anyone).
- Submitting any other person’s work. This includes joint work.
The lecturer may vary the assessment scheme after inspecting the assignment submissions but it will remain broadly similar to the description above.
The work you submit must be your own work. Submission of work partially or completely derived from any other person or jointly written with any other person is not permitted.
The penalties for such an offence may include negative marks, automatic failure of the course and possibly other academic discipline. Assignment submissions will be examined both automatically and manually for such submissions.
Relevant scholarship authorities will be informed if students holding scholarships are involved in an incident of plagiarism or other misconduct.
Do not provide or show your assignment work to any other person — apart from the teaching staff of COMP6771.
If you knowingly provide or show your assignment work to another person for any reason, and work derived from it is submitted, you may be penalized, even if the work was submitted without your knowledge or consent. This may apply even if your work is submitted by a third party unknown to you.
Note you will not be penalized if your work has the potential to be taken without your consent or knowledge.
This assignment is due Monday 3rd of August, 19:59:59. Submit the assignment using the following comand while logged into the CSE machines:
This will submit whatever is on the master branch of THIS repository (the one this README.md file is contained in) at the moment of submission.
Please ensure that you can build and run your tests successfully on the CSE machine.
If your assignment is submitted after this date, each hour it is late reduces the maximum mark it can achieve by 2%.
For example if an assignment you submitted with a raw awarded mark of 85% was submitted 5 hours late, the late submission would have no effect (as maximum mark would be 90%).
If the same assignment was submitted 20 hours late it would be awarded 60%, the maximum mark it can achieve at that time.
There appears to be a bug in the configuration stage for the CMake Tools VS Code extension, when you already have an existing build cache (build directory). It's unlikely that this bug will be fixed before the term ends, so we've updated the project settings for VS Code so that CMake Tools won't auto-configure for you any more, but this means that you'll manually need to run this very specific configuration step.
Instead of running CMake: Configure
when you press Ctrl+Shift+P, you'll need to run CMake: Delete Cache and Reconfigure
. This will delete your build cache and require you to rebuild everything, but at least it will work without major confusion.
The critical part to remember is that unlike in Assignments 1 and 2 where you could add a new file and it would automatically reconfigure, you will need to re-run this step every single time you edit any CMakeLists.txt
.