Conceptual Questions - Simple Classes & ADTs

Thomas J. Kennedy

Contents:

1 Overarching Questions

When designing or analyzing or writing code there are many overarching questions one needs to keep in mind. The next few questions were asked by your colleagues in previous semesters. An entire lecture (or two) could be dedicated to answering and discussing these questions.

1.1 Writing Constructors

Constructor: How do I determine what to include in the Constructor?

Everything. Your mindset when writing a constructor must be: I defined a Thing. What do I need to initialize to have a complete (and conceptually valid) Thing? Consider

class Thing {
    private:
        int         anInteger;
        std::string name;
        double      cost;

        double*     anArray;

        //...

    public:

        Thing();

        Thing(std::string name, int aNum);

        //...
};

In our example Thing class we have four attributes (specifically, four private data members). We need to guarantee that all four are set to appropriate initial values. This leads us to a fundamental Constructor rule–one I have stated throughout my examples.

Every Constructor must initialize every attribute. If Thing has 4 pieces of private member data, all 4 must be initialized. In our Thing example we would end up with something similar to

Thing::Thing()
  :name("None")
{
    anInteger = 0;
    cost      = 0.0;
    anArray   = nullptr;
}

Notice how I chose zero for each datatype (except std::string)? This is a choice that one must make based on the underlying problem (or codebase). I chose "None" for name for the same reason I often use "Empty" or "Generic"

I am a human being.

Recognizing a word is more natural than recognizing a lack of value (e.g., an empty string). Let us implement the Non-Default Constructor:

Thing::Thing(std::string name, int aNum)
{
    this->name = "This thing is called " + name + "!";

    anInteger = aNum * 337;
    cost = 72 + aNum / 3.14;

    anArray = new double[anInteger];
}

Notice this less obvious use of name and aNum? There is no rule saying how each of these attributes must be initialized. However, the initial values must make sense. To determine what makes sense, we must understand the problem domain and what Thing represents.

1.2 Pass-by-Reference vs Pass-by-Value

References: How do I determine what to pass by reference and what not to pass by reference? Do I pass everything by reference?

Cost. How expensive is it to copy what you are passing? Primitive types such as int, double, and char, are cheap (i.e., introduce little overhead).

If you are passing a non-trivial data structure, pass-by-reference (read/write access) or pass-by-const-reference (read only access) will avoid the overhead of a deep-copy.

1.3 Construct Additional Pylons Pointers

Pointers: when do I want to use pointers? as much as possible? Are there any cases where I absolutely don’t want to use pointers?

Use pointers when you want to:

If you can avoid using pointers, without introducing substantial overhead, do not use pointers. If you can get away with reference variables e.g.,

Thing& somethingCool = aDifferentThingInstance;

or leverage move-semantics (a topic not covered formally in any 100, 200, or 300 level courses) do not use pointers.

Pointers introduce the cost of a de-reference operation (i.e., we must first go to the block of memory before performing our desired operation). Of course, we can side step this issue with something along the lines of:

LinkedList* list = new LinkedList();

const LinkedList& printAccess = someList;  // read-only access
LinkedList& justSwitchToRust = someList;   // read-and-write access

1.4 Struct vs Class

Struct or Class: How do I determine when to use a struct or when to use a class?

Encapsulation and JBOD.

A struct comes from the C-language, where it was a wrapper for data and nothing more.

If you are simply bundling together data, use a struct (consider the Node struct from my examples). If you have a more complex interface, use a class (consider the Linked List in my examples).

TL;DR - If you have private data and a definable interface, use a class. If you are going to leave everything public without member functions, use a struct.


2 Lambdas - When and When Not?

Lambda (or anonymous) functions are a fairly new C++ addition (from the C++11 standard). Before we use lambda functions, we need to ask the following:

  1. Why?
    • Why is a lambda function necessary here?
    • Why not use a proper method?
  2. Am I using… ?
  3. Am I using iterators?
  4. Is this a one-off function? (Consider the Java Listener interface)
  5. Does this lambda function take (implicit or explicit) arguments? Or does it rely exclusively on a capture clause?
  6. When using a lambda simplify readability of the code?
  7. Will using a lambda conceivably (more aptly possibly) prevent compiler optimizations (e.g., loop unrolling, inlining, out-of-order execution, or speculative execution)?
  8. Will the lambda belong in the scope of a function/method that will be called multiple times?
    • Is it possible to broaden the scope of this lambda?
  9. Does this lambda wrap a function call? Should I consider std::bind instead?

This list has grown beyond my original expectations. How about two simple questions:

  1. Am I working with low-level (the bottom) or high-level (the top) parts of the codebase?
  2. Did I get the answer from StackOverflow?

3 UML Class Diagrams

Is the UML Class diagram from Review 03 readable?

Click Image to View Full-Size

overview.puml
@startuml
skinparam classAttributeIconSize 0
hide empty members


package "Data Structure" {
    class Node << (T, #00AAFF) Template >> {
        + data: T
        + next: Node*
    }

    class LinkedList << (T, #00AAFF) Template >> {
        - head: Node*
        - tail: Node*
        - currentSize: int

        + push_back(date: T) -> void
        + size() -> int

        + begin() -> iterator
        + end() -> iterator
        + begin() -> const_iterator
        + end() -> const_iterator
    }

    class NaivePool << (T, #00AAFF) Template >>  {
        - thePools: T[][]
        - numPools: int
        - blocksPerPool: int
        - nextAvailBlock: pair<int, int>

        + NaivePool(bSize: int = 8, preAlloc: int = 1)
        + ~NaivePool()
        + getNext() -> T*
        - reserveNext()
    }

    class LinkedList::Iterator << (T, #00AAFF) Template >>  {

    }
}

package "The Actual Problem" {
    class Room::Flooring {
        + type: String
        + unitCost: Cost

        + Flooring(n: String = "Generic", c: Cost = 1.00)
    }

    class Room::DimensionSet {
        - length: Dimension
        - width: Dimension

        + DimensionSet(l: Dimension = 1, w: Dimension = 1)
        + setLength(v: Dimension)
        + getLength() -> Dimension
        + setWidth(v: Dimension)
        + getWidth() -> Dimension
    }

    class Room {

    }

    class House {

    }
}

package std {
    Interface Iterator {

    }

    Interface Container {

    }

    Interface Allocator {

    }
}

NaivePool -[#blue]> Node: "handles allocation of"
NaivePool -[#blue]> LinkedList: "allocates Nodes for"
LinkedList o-Node

LinkedList::Iterator --> Node
LinkedList::Iterator <-- LinkedList: "provides"

LinkedList::Iterator .[#green].|> Iterator: "partially"
LinkedList .[#green].|> Container: "partially"

NaivePool .[#green].|> Allocator: "fakes"

House -[#DarkSlateGrey]--> "container" LinkedList
House o-- Room

House .[#green].|> Container: "partially"

Room o-- "flooring" Room::Flooring
Room o-- "dimensions" Room::DimensionSet 

@enduml