I don't know if I'll be teaching the course again, but if I wait until next year I'll forget feedback I've received. Note that more test cases have been added (if you pull the new starter code); those are for the (potential) future!
The C++ information on the Lab 0 page are some moderately advanced topics for those ready to start with the lab. This page includes some information for those with no or little familiarity/past experience with C++.
Note that ultimately this is an ECE course, and ECE students have a standard course in C++ in second year; this is not a complete replacement for a formal source on C++, but just a collection of some C++ concepts to hopefully help you pick up the language more smoothly.
Again, I am happy to be contacted if someone has specific questions, as long as the student has put in their own effort to solve the problem.
This page still assumes knowledge of C (primarily for its concept of pointers); both ECEs and EngScis at UofT learn C in first year.
Miscellaneous differences between C and C++.
In C, one can group data into a struct
like so.
struct Foo {
int bar;
char* baz;
};
In C, struct
s (and enum
s and union
s) are in their own namespace;
to declare a variable or field of type Foo
(defined above), prefixes the struct
(/enum
/union
) name with the corresponding keyword:
struct Qux {
struct Foo* foo;
};
To avoid this, it’s common to do something like typedef struct Foo Foo;
which brings struct Foo
into the global namespace as Foo
; or in one definition:
typedef struct Foo {
int bar;
char* baz;
} Foo;
Note that the first appearance of Foo
may be omitted in this case, in which case the above code typedef
s an anonymous struct definition as Foo
in the global namespace.
If done so, the above definition cannot be accessed as struct Foo
anymore.
Note that (in C), to define a recursive struct, we cannot use the typedef
ed name in a field; the typedef
is not “active” yet at the time of the struct definition.
So a recursive struct must be named, even if it is typedef
ed.
typedef struct LinkedList {
struct LinkedList* previous; // works
LinkedList* next; // doesn't work
} LinkedList;
In C++, there is no separate namespace for struct
/enum
/union
s; the following is sufficient in C++.
struct LinkedList {
LinkedList* previous;
LinkedList* next;
};
C++ also introduces the class
keyword.
class
defines a type like struct
; the only difference is the default visibility.
By default, fields (and functions) in a struct
are public
; they can be accessed by anyone.
By default, fields (and functions) in a class
are private
; they can only be accessed within functions within the same class (and friend
s, but we will ignore that in this primer).
One can change the visibility of items like so.
struct Foo {
int bar; // public
char* baz; // public
private:
float f1; // private
float f2; // private
public:
float f3; // public
float f4; // public
};
It is not uncommon to prefix field names with m_
, as is done in the starter code.
In C, function names are all in a global namespace and unique. In C++, functions can have the same name as long as their parameters are (“sufficiently”) different (more on this later). Functions cannot be overloaded based on their return type.
The type int*
in C and C++ is a pointer to an integer.
The type int&
in C++ is a reference to an integer.
Underneath the hood, a reference is also just a pointer.
However, among other subtle differences:
*
); and&
).Similar to pointers, int const*
is a pointer to a constant integer, and int const&
is a reference to a constant integer.
In terms of overload resolution, a reference “behaves” the same as a value. For example, the third function definition below is compatible with (different from) the first two, but the first two are not compatible with each other.
void print(int x) {
printf("%d\n", x);
}
void print(int& x) {
printf("%d\n", x);
}
void print(float f) {
printf("%f\n", f);
}
One of the most obvious additions in C++ over C is object-oriented programming (OOP), which is also a common paradigm in Java and Python (two popular languages you may be already familiar with).
In C++/OOP, one can define functions specific to a type. Let’s consider the following.
class LinkedList {
LinkedList* previous; // private
LinkedList* next; // private
public:
int data; // public
void insert_before(LinkedList* new_node) { // public member/instance function inline definition
LinkedList* old_previous = this->previous;
new_node->previous = old_prevoius;
new_node->next = this;
this->previous = new_node;
old_prevoius->next = new_node;
}
void insert_after(LinkedList* new_node); // public member/instance function declaration
};
// member/instance out-of-line function definition
void LinkedList::insert_after(LinkedList* new_node) {
LinkedList* old_next = this->next;
new_node->previous = this;
new_node->next = old_next;
this->next = new_node;
old_next->previous = new_node;
}
First, node that function parameter names may be omitted if unused (e.g. in a declaration), as is done in the starter code, to avoid duplication (of the parameter names).
But above, the parameter of insert_after
is named, which is probably more common.
Functions may be defined inline in the struct
or class
definition, as in the case of insert_before
above.
Functions defined in a struct
or class
definition are implicitly inline
(may show up in multiple compilation units, e.g. be in a header file that is included in multiple .cpp
files).
An out-of-line function definition on a type is similar to a (freestanding) function definition in C, except we prefix the function name with the type’s name and ::
- e.g. LinkedList::
before insert_after
above.
Note that all functions on a type must be declared (and possibly defined) in its struct
or class
definition.
Out-of-line function definitions should not appear in header files.
Functions in a type not declared as static
have an implicit this
parameter.
Such functions can only be called on a value or pointer of said type.
Similar to accessing a field in C, to call a function on a value, one uses the dot (.
) operator;
to call a function on a pointer, one can explicitly dereference the pointer (*
) to obtain a value, then use the dot operator to perform the call;
alternatively, one can use the arrow (->
operator).
The special this
value is always a pointer of said type; hence code like this->previous
and this->next
above.
If a function on a type is declared as static
(before the return type), then it is not called on a value of said type, but on the type itself, and there is no implicit this
parameter.
It is called by prefixing the function name with the type’s name and ::
.
#include <cmath>
#include <cstdio>
class Math {
public:
static double log3(double f) {
return std::log2(f) / std::log2(3.0);
}
};
int main() {
printf("log base 3 of 81 is %f.\n", Math::log3(81.0));
return 0;
}
C++ types may have constructors, which are dedicated functions to construct a value of a type.
A constructor, as a function, has no explicit return type/value (it implicitly returns the object being created), and is always named the same name as the type.
Constructors obey the same visibility rules as other, “regular” functions; e.g. one can have a private
constructor which is only used internally.
Because of function overloading, we may add the following two constructors to the above LinkedList
type definition.
class LinkedList {
// previously shown fields and functions omitted
public:
LinkedList() { // constructor/function inline definition
this->previous = this;
this->next = this;
this->data = 0;
}
LinkedList(int data); // constructor/function declaration
};
// constructor/function out-of-line definition
LinkedList::LinkedList(int data) {
this->previous = this;
this->next = this;
this->data = data;
}
The only way for a constructor to “fail” is by throwing an exception.
Many codebases forbid/disable exceptions, because of complications they bring.
Similarly, we won’t require/be using exceptions; if construction may fail, define a static
function which performs the fallible steps,
then calls a private constructor which is infallible (e.g. just initializes its fields to arguments passed in).
A constructor can initialize its fields “directly” like so (alternate definition of LinkedList::LinkedList(int data)
from above).
// NOTE/SEE BELOW: use curly braces instead of parentheses to initialize the fields
LinkedList::LinkedList(int data) : previous(this), next(this), data(data) {
// nothing else to do
}
Note that curly braces may be used instead of parentheses when initializing the fields, and as described in Lab 0, that is preferable, and is done in the starter code.
A destructor is another special function on a type, that is called when the object goes out of scope (for stack-allocated items (local variables)) or is deallocated (for heap-allocated items).
There is always exactly 1 destructor for a type; if not explicitly defined, the compiler generates one by calling the destructor for each field of the type.
The special name (if you need to define your own destructor) of a destructor is a tilde (~
) followed by the type name.
Generally, you should not need to define your own destructor, or if you do, it should be defaulted like so (as in the starter code).
Compilation::~Compilation() = default;
This is because the default destructor will call the destructor of all your fields.
For example, if you have a heap allocated field in a std::unique_ptr
, std::unique_ptr
already deallocates its value in its destructor.
Similarly, std::vector
’s destructor calls the destructor for all its elements, and then frees its memory.
The idea that the constructor automatically calls the destructor for local variables when they go out of scope is very important to C++, and (poorly) named as Resource Acquisition Is Initialization (RAII).
In C++, it is possible to define types or functions that are generic, or in C++, these are called templates.
For example, we can “generalize” the LinkedList
definition to store data of any type like so.
template<typename T> LinkedList {
// other fields and functions omitted
T data;
};
std::vector
in the standard library, which is a dynamically growable array, is defined similarly.
To use a templated type, we specify the “template argument” after the type name in angled brackets, e.g. LinkedList<int>
or std::vector<std::string>
.
Note that templated types/functions with different template arguments are distinct; while it is not possible to overload a function based on a return type, the following is possible,
because get_default
(below) when “instantiated” with a float
is a completely different name than when instantiated with an int
.
// templated function declaration
template<typename T> T get_default();
// specialized template function definition
template<> int get_default() {
return 0;
}
// specialized template function definition
template<> float get_default() {
return 0.0;
}