- 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 Iterator access [gdwg.iterator.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]
- 2022-07-26: Clarified
operator->and printing empty graphs (see operator-> and printing empty graphs) - 2022-07-18: Fixed a typo in
insert_edge(see this post for details) - 2022-07-14: Clarified gdwg.internal (see this post for details)
- 2022-07-11: Initial Release
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
Fin code using the term Equivalent to. The semantics forFare interpreted as follows:- All of the above terminology applies to the provided code, whether or not it is explicitly specified. [Example: If
Fhas 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
Fhas a non-voidreturn 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());
- Preconditions: Type
InputItmodels Cpp17InputIterator and is indirectly readable as typeN. - Effects: Initialises the graph’s node collection with the range
[first, last).
- Postconditions:
*thisis equal to the valueotherhad before this constructor’s invocation.other.empty()istrue.- All iterators pointing to elements owned by
*thisprior to this constructor’s invocation are invalidated. - All iterators pointing to elements owned by
otherprior 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:
*thisis equal to the valueotherhad before this operator’s invocation.other.empty()istrue.- All iterators pointing to elements owned by
*thisprior to this operator’s invocation are invalidated. - All iterators pointing to elements owned by
otherprior to this operator’s invocation remain valid, but now point to the elements owned by*this.
- Returns:
*this.
- Postconditions:
*this == otheristrue.
Postconditions:
*this == otheristrue.- All iterators pointing to elements owned by
*thisprior to this operator’s invocation are invalidated.
Returns:
*this.
Effects: Adds a new node with value
valueto the graph if, and only if, there is no node equivalent tovaluealready stored.Postconditions: All iterators are invalidated.
Returns:
trueif the node is added to the graph andfalseotherwise.
Effects: Adds a new edge representing
src→dstwith 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:
trueif the edge is added to the graph andfalseotherwise.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. [Note: Unlike Assignment 2, the exception message must be used verbatim. —end note]
Effects: Replaces the original data,
old_data, stored at this particular node by the replacement data,new_data. Does nothing ifnew_dataalready exists as a node.Postconditions: All iterators are invalidated.
Returns:
falseif a node that contains valuenew_dataalready exists andtrueotherwise.Throws:
std::runtime_error("Cannot call gdwg::graph<N, E>::replace_node on a node that doesn't exist")ifis_node(old_data)isfalse. [Note: Unlike Assignment 2, the exception message must be used verbatim. —end note]
Effects: The node equivalent to
old_datain the graph are replaced with instances ofnew_data. After completing, every incoming and outgoing edge ofold_databecomes an incoming/ougoing edge ofnew_data, except that duplicate edges shall be removed.Postconditions: All iterators are invalidated.
Throws:
std::runtime_error("Cannot call gdwg::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: Unlike Assignment 2, the exception message must be used verbatim. —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.Returns:
trueifvaluewas removed;falseotherwise.Postconditions: All iterators are invalidated.
Effects: Erases an edge representing
src→dstwith weightweight.Returns:
trueif an edge was removed;falseotherwise.Postconditions: All iterators are invalidated.
Throws:
std::runtime_error("Cannot call gdwg::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. [Note: Unlike Assignment 2, the exception message must be used verbatim. —end note]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
iprior to the element being erased. If no such element exists, returnsend().Postconditions: All iterators are invalidated. [Note: The postcondition is slightly stricter than a real-world container to help make the assingment easier (i.e. we won’t be testing any iterators post-erasure). —end note]
Effects: Erases all edges between the iterators
[i, s).Complexity O(d), where d=
std::distance(i, s).Returns: An iterator equivalent to
sprior to the items iterated through being erased. If no such element exists, returnsend().Postconditions: All iterators are invalidated. [Note: The postcondition is slightly stricter than a real-world container to help make the assingment easier (i.e. we won’t be testing any iterators post-erasure). —end note]
Effects: Erases all nodes from the graph.
Postconditions:
empty()istrue.
Returns:
trueif a node equivalent tovalueexists in the graph, andfalseotherwise.Complexity: O(log (n)) time.
- Returns:
trueif there are no nodes in the graph, andfalseotherwise.
Returns:
trueif an edgesrc→dstexists in the graph, andfalseotherwise.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. [Note: Unlike Assignment 2, the exception message must be used verbatim. —end note]
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
srctodst, 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. [Note: Unlike Assignment 2, the exception message must be used verbatim. —end note]
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. [Note: Unlike Assignment 2, the exception message must be used verbatim. —end note]
- Returns: An iterator pointing to the first element in the container.
Returns: An iterator denoting the end of the iterable list that
begin()points to.Remarks:
[begin(), end())shall denote a valid iterable list.
Returns:
trueif*thisandothercontain exactly the same nodes and edges, andfalseotherwise.Complexity: O(n + e) where n is the sum of stored nodes in
*thisandother, and e is the sum of stored edges in*thisandother.
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{};
for (const auto& [from, to, weight] : v) {
g.insert_node(from);
g.insert_node(to);
g.insert_edge(from, to, weight)
}
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 ]
Note: The empty graph should print an empty string. i.e. auto g = graph<int, int>();
auto oss = std::ostringstream{};
oss << g;
CHECK(oss.str().empty());
template<typename N, typename E>
class graph<N, E>::iterator {
public:
using value_type = graph<N, E>::value_type;
using reference = value_type;
using pointer = void;
using difference_type = std::ptrdiff_t;
using iterator_category = std::bidirectional_iterator_tag;
// Iterator constructor
iterator() = default;
// Iterator source
auto operator*() -> reference;
// auto operator->() -> pointer not required
// 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>::iteratormodelsstd::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: Returns the current
from,to, andweight.
- Effects: Advances
*thisto the next element in the iterable list.
[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
*thisto the previous element in the iterable list.Returns:
*this.
- Effects: Equivalent to:
- Returns:
trueif*thisandotherare pointing to the same elements in the same iterable list, andfalseotherwise.
Your graph is required to own the resources (nodes and edge weights) 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 node, you are only allowed to have one underlying resource (heap) stored in your graph for it. This means every
Ncan only be stored once per graph instance.For each edge, you should avoid using unnecessary additional memory wherever possible.
[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.
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.
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
constmember function; or - A non-
constmember function; or - Both a
constand 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.
For convenience's sake, inside the graph class you will likely have a member type value_type, defined like so:
class graph {
public:
struct value_type {
N from;
N to;
E weight;
};
// ...
};
As noted in the compulsory internal representation section, you are unlikely to want to use this directly within your representation. However, it is used by the iterator type, and is good practice to have for a container.
