Language notes

Some language-specific issues with C# and with C++




Implementing an Equals method

Let us implement an .Equals() method in the class StrList. Note how two StrLists are equal if they have the same length and if the corresponding values are equal up to that length.

    public override bool Equals(System.Object obj) {
        if (obj == null) return false;

        // If parameter cannot be cast to StrList return false.
        StrList s2 = obj as StrList;	// safe downcast
        if (s2 == null) return false;

        // now we check the actual data:
        if (currsize != s2.currsize) return false;
	for (int i=0; i< currsize; i++) {
		//if (!(elements[i].equals(s2.elements[i]))) return false;
		if (!(elements[i]==s2.elements[i])) return false;
	}
	return true;
    }
For this to work properly in all contexts, we also have to implement GetHashCode(), below.

TList doesn't have a remove() operation, so all unused slots are always null. However, we could easily add remove(), and have two TLists with the same currsize and same data up to that point, but wildly different data beyond that point.

GetHashCode():  There is a tricky issue regarding .Equals(). Some (most) built-in data structures in C# (and in Java, with .hashCode()) just assume that if s1.GetHashCode() != s2.GetHashCode(), then !s1.Equals(s2) (ie s1 ≠ s2). But if GetHashCode() is implemented for StrList by adding up the GetHashCode() values of all the cell contents, even beyond currsize, this will fail! (This is done so the language can use different hashcodes as a fast way of determining that two objects are not .Equals() to each other).




C# operator overloading

C# supports redefining operators in some contexts. For example, if you define a class Bignum, you can define + to work for Bignums:

    Bignum f = fact(100);
    Bignum g = exp(2,100);

    Console.WriteLine( f+g );        // versus f.Add(g)

You do this with something like this, in the class Bignum:

    public Bignum Add( Bignum second) {
        ...
    }

    public Bignum operator + (Bignum second) {
        return Add(second);
    }
       
In essence, f+g is now treated as if it were f.operator+(g), that is, like f.Add(g).



hashtable enumerator: demos/dictionary.cs

I want this to work:
	foreach (KeyValuePair<string,int> kvp in d) 
Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
The hashtable is an array of linked lists; the linked-list cell type is
    public class Cell {
	private K key_;
	private V val_;
	private Cell  next_;
	public Cell(K k, V v, Cell n) {key_ = k; val_ = v; next_ = n;}
	public K getKey() {return key_;}
	public V getVal() {return val_;}
	public Cell   next() {return next_;}
        public void setVal(V v) {val_ = v;}
        public void setNext(Cell c)   {next_ = c;}
    }	
To start, I must have class dictionary<K,V> inherit from System.Collections.Generic.whatever. This works:
	class dictionary<K,V> : System.Collections.Generic.IEnumerable<KeyValuePair<K,V>> {
Then I must implement the IEnumerable method. The exact method signature is as follows; note the return type.
    IEnumerator<KeyValuePair<K,V>> IEnumerable<KeyValuePair<K,V>>.GetEnumerator() {
	return foonumerator();
    }
What is up with foonumerator()? That's here:

    IEnumerator<KeyValuePair<K,V>> foonumerator() {
	for (int i = 0; i<htablesize; i++) {
	    Cell p = htable[i];
	    while (p != null) {
		yield return new .KeyValuePair<K,V>(p.getKey(), p.getVal());
		p = p.next();
	    }
	}
	yield break;
    }
Why didn't I just define this in IEnumerable, above? Because we also must implement the non-generic form of IEnumerable, due to inheritance constraints. I did that this way:
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { 
	return foonumerator(); 
    }
Otherwise I would have to type everything twice.

I figured this all out by reading the MSDN Dictionary.cs reference code, here.


Introduction to C++

Here are a few notes on this: Intro to C+++

What about installing it?

Macs sometimes have xcode. Or you can get it at https://developer.apple.com/xcode/ (or maybe the Apple App Store).

For windows, you can install MS Visual Studio, or mingw. The link to the MSDNAA site for Visual Studio is http://e5.onthehub.com/WebStore/ProductsByMajorVersionList.aspx?ws=afe1b6ef-7d9b-e011-969d-0030487d8897&vsro=8/.

Be sure to click register the first time you connect. Your account identifier is your Loyola email address, with the "@luc.edu".



The C++ Memory Problem

Remember List.cpp? I wanted you to write a linked-list destructor.
    ~LinkedList() {
	if (head == NULL) return;
	cout << "calling destructor on LinkedList:" << endl;
	printList();
	Cell<T> * q = head;		// we checked above head != null
	Cell<T> * p = q->next();
	
	while (q != NULL) { 
	    cout << "deleting " << q->data() << endl;
	    delete q;
	    q = p;
	    if (p != NULL) p = p->next();
	}
	cout << "done with destructor" << endl;
    }
(demo of lab3/pldlinkedlist)

How does this destructor get triggered? The lists go out of scope; that is, when we get to the } for the scope in which the list was declared, it gets destructed.

Alas, this is not really enough. Suppose we create a method to return pointers to new list objects:

    LinkedList<T> * p = mylistmaker();

Now when p goes out of scope, the destructor is not called, because we might have assigned the list to another variable:
  
    hashtable[i] = p;

But this chain is hard to keep track of. How can we, in C++, be sure that an object is deleted when it should be?

One method is iron discipline: we carefully document that callers of mylistmaker(), above, must be careful to delete the object pointed to.

Another mechanism is to allow memory leakage. I mean, how long will it take to use up 4 GB?

Smart Pointers

(Much of this material comes from Using C++11's Smart Pointers, by David Kieras of the University of Michigan.)

Yet another strategy, though, is so-called smart pointers: we create a Pointer object, and overload the (unary) * and -> operators. When a smart pointer goes out of scope, the pointer is deconstructed. However, we put into the object pointed to a reference counting mechanism (there are some other smart-pointer implementations, but this is the most common).

Here are the rules:
As long as we can control assignment to (and initialization of) the smart-pointer variables, the reference counts are easy to implement. When an object has its reference count reach zero, it is deconstructed.

This is not perfect (because of the possibility of "looped" pointers, forming a cycle), but in practice it works quite well.

Here is some code involving raw pointers p,

LinkedList<T> * p = mylistmaker();
LinkedList<T> * q = p;
{
    LinkedList<T> * r = mylistmaker();
    r = p;                    // what happens to the object created by the line above?
}
q = NULL;                // what happens to the object q pointed to before?
p = mylistmaker();    // now what happens?

What could go wrong?

C++ actually contains three types of smart pointer:

shared_ptr

The reference-counting smart pointer described above is shared_ptr.

Here is some code:

    Thing * p = new Thing();                  // raw pointer
    shared_ptr<Thing> ps(new Thing());    // ps is constructed from a raw pointer

We can now do things like call (*ps).foo(), or ps->foo(). This amounts to overloading the * and -> operators.

We can even do this:

    shared_ptr<Thing> ps1 (new Thing());
    ps = ps1;        // old Thing pointed to by ps gets deleted (or ref count gets decremented); new Thing gets its refcount incremented

Now consider this:

    shared_ptr<Thing> ps2 = new Thing();    // equiv to the creation above of ps, but suspicious

Why is this suspicious? Compare it to the following (recall p is a raw pointer to a Thing):

    shared_ptr<Thing> ps3 = p;                // this is dangerous!

In ps3, nothing prevents us from passing p to some other part of the program, or calling delete p.This messes up the sharing count achieved via ps3. (For the record, note that the above represents a call to the constructor for ps3, and is not an assignment.)

What about a derived class ThingSpawn?

    Thing* p = new ThingSpawn();    // legal
    shared_ptr<Thing> ps(new ThingSpawn());    // also legal!

When we run

    shared_ptr<Thing> ps (new Thing());

there are two object allocations going on: first we create the new Thing(), and then we create the new smart pointer ps. Life would be faster if we could combine these, which we can do like this:

    shared_ptr<Thing> ps (make_shared<Thing>());
    shared_ptr<Thing> ps (make_shared<ThingSpawn>());

What's really going on with a shared_ptr is that it points literally to a manager object, which in turn points to the managed object, which is the Thing in the examples above:

    ps  ---->  manager[count=1] -----------> Thing

When we add a couple pointers

    shared_ptr<Thing> ps4 = ps
    shared_ptr<Thing> ps5 = ps

we then have a picture like this:

    ps--------------->  manager[count=3] ---------> Thing
    ps4------------>----/     /
    ps5------------>---------/

weak_ptr

Recall that if we create a ring of shared_ptr, the object will not get deleted when the "external" references hit zero, because there are still "internal" references. There is another type of pointer, weak_ptr, that attempts to provide some flexibility here. In the managers diagrammed above, the count refers to the shared_ptr count; all shared_ptr managers also include a field to count weak_ptrs.

When the shared_count hits zero, the managed object is deleted, but if the weak_count is nonzero, the manager object is retained. This allows a later query via the weak_ptr to see if the managed object (the Thing) still exists.

Here's a list of ways to initialize a weak_ptr (from ):

shared_ptr<Thing> sp(new Thing);         // create shared_ptr
weak_ptr<Thing>   wp1(sp);               //
weak_ptr<Thing>   wp2;                   // wp2 points to nothing (yet)
wp2 = sp;                                // legal; wp2=wp1 would also be legal
weak_ptr<Thing> wp3(wp2);                // construct wp3 from wp2
wp1.reset();                             // now wp1 is uninitialized, though wp2 and wp3 still point to sp.

We can check if the managed object pointed to by a weak_ptr wp still exists by checking if (wp), or by calling wp.expired(). If wp is still valid, we can resurrect the Thing it points to with

    shared_ptr<Thing> sp = wp.lock();

If we have two objects C1 and C2 in which each object has a shared_ptr to the other, then when C1 and C2 go out of scope, the shared_ptr values keep them alive. If we'd used weak_ptrs, then C1 and C2 would be deallocated.

If we're trying to build a list structure CL that contains nodes forming a circular list, we can't use weak_ptrs, because nothing else points to the nodes. But what does make sense in this case is for the CL destructor to know about the circularity (given that the circularity is "private" to CL), and take explicit steps to delete every node.

unique_ptr

These are lightweight versions of shared_ptr, for the case when we don't actually want to do any sharing. The object pointed to by a unique_ptr gets deconstructed when the unique_ptr goes out of scope. If we create a unique_ptr up:

    unique_ptr<Thing> up (new Thing);
   unique_ptr<Thing> up(make_unique<Thing>());

then we're not allowed to assign up to another weak_ptr, or use it to construct another pointer.

Unique_ptr values can be moved from one unique_ptr to another.