Recall: auto - automatic type deduction, <=> - 3-way comparison
auto results = s1 <=> s2;
Q: How can we support <=> in our own classes?
struct Vec {
int x, y;
auto operator<=>(const vec &other) const {
auto n = x <=> other.x;
return (n == 0)? y<=>other.y: n;
}
};
// Now we can say
Vec v1{1, 2}, v2{1, 3};
v1 <=> v2
// But we can also say
v1 <= v2 // etc..
// The 6 relational operatos: **<**, **<=**, **==**, **!=**, **>**, **>=
//** automatically rewritten in terms of **<=>**
eg. v1 <=> v2 -----> (v1<=> v2) <=0
6 operators for free! But you can also sometimes get operator for <=> free!
struct Vec {
int x, y;
auto operator<=>(const Vec &other) const = default;
// "default" does lexicographic ordering on fields of Vec.
// - equivalent to what we wrote before
}
}
What might the default behaviour not be correct?
struct Node {
int data; // lex order on these fields would
Node *next; // compare ptr values - not useful!
};
<aside> 🧠 Null ptr is object and cannot have method
</aside>
struct Node {
int data;
Node *next; // Note: works for non-empty lists
auto operator<=>(const Node &other) const {
auto n = data <=> other.data;
if(n != 0) return n;
if(!next &&(other.next)) return n; // n already equal
if(!next) return std::strong-ordering::less;
if(!other.next) return std::strong-ordering::greater;
return *next <=> *other.next;
}
};
<aside> 💡 Invariants(不変性) + Encapsulation(カプセル化)
struct Node {
int data;
Node *next;
...
~Node(){delete next;}
};
Node n {2, nullptr};
Node m {3, &n};
What happens when these go out of scope?
Class Node relies on an assumption for its proper operation: that next is either nullptr or was allocated by new.
We can’t gurantee this invariant - can’t trust the user to use Node property
ex. stack - invariant - last item pushed is first item popped - but not if the client can rearrange the underlying data
Hard to reason about programs if you can’t rely on invariants.
</aside>
<aside> 💡 To enforce invariants, we introduce encapsulation
struct Vec { // ↓ also public: default visibility
Vec (int x, int y); // = public
private: // can't be accessed outside of struct Vec
int x, y;
public: // anyone can access
Vec operator+(const Vec &other);
...
};
In general: want private fields; only methods should be public Better to have default visibility = private
switching from struct to class… ↓
class Vec {
int x, y;
public:
Vec(int x, int y);
Vec operator+(const Vec &other);
...
};
Let’s fix our linked list class
// list.cc
export class list { // externally, this is struct list::Node
struct Node; // **private** nested class - only accessible within class list
Node *theList = nullptr;
public:
void addToFront(int n);
int &ith (int i); // means we can do list.ith(4) = 7;
~List();
...
};
// list-impl.cc
struct list::Node { // Nested class
int data;
Node *next;
...
~Node(){delete next;}
};
void List::addToFront(int n) {
theList = new Node{n, theList};
}
int &list::ith(int i) {
Node *cur = theList;
for(int j = 0; j < i; ++j) cur = cur->next;
return cur->data;
}
List::~List(){delete theList;}
Iterator - loop over something
Only List can create/manipulate Node objs now.
→ can gurantee the invariant that next is always either nullptr or allocated by new
<aside> 💡 Iterator Pattern
<aside> 💡 SE Topic: Design Patterns
certain programming challenges arise often
Sol’n - Iterator Pattern:
Recall(c):
for(int *p = arr; p! = arr + size; t*p)
printf("%d", *p);
class List {
struct Node;
Node *theList = nullptr;
public:
class Iterator {
Node *p;
public:
Iterator(Node *p): p{p}{}
Iterator &operator++(){ p = p->next; return *this; }
bool operator != (const Iterator other) const {
return p! = other.p;
}
Int &operator*() { return p->data; }
}
Iterator begin(){ return Iterator{theList}; }
Iterator end(){ return Iterator{nullptr}; }
};
</aside>
</aside>