Conceptual Questions - Simple Classes & ADTs
Thomas J. Kennedy
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:
- perform memory allocations directly (e.g., use
new
,delete
,malloc
, orrealloc
) - pseudo-resize an array
- work with c-strings
- perform low-level optimizations
- build non-trivial data structures.
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.
-
Encapsulation refers to containing all the data in a single struct/class and limiting access through member functions.
-
JBOD is a term I stole from RAID (storage) literature. It traditionally means Just a Bunch of Disks. I use it slightly differently… Just a Bunch of Data.
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:
- Why?
- Why is a lambda function necessary here?
- Why not use a proper method?
- Am I using… ?
std::for_each
std::transform
std::accumulate
std::copy
std::find_if
std::max_element
std::min_element
std::min_max_element
std::lexicographical_compare_three_way
- Another function from the Algorithm Library
- Am I using iterators?
- Is this a one-off function? (Consider the Java Listener interface)
- Does this lambda function take (implicit or explicit) arguments? Or does it rely exclusively on a capture clause?
- When using a lambda simplify readability of the code?
- Will using a lambda conceivably (more aptly possibly) prevent compiler optimizations (e.g., loop unrolling, inlining, out-of-order execution, or speculative execution)?
- 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?
- 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:
- Am I working with low-level (the bottom) or high-level (the top) parts of the codebase?
- 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
@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