UofT ECE 467 (2022 Fall) - C++ Primer



The website and starter code is being pre-emptively updated!

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.

General Differences

Miscellaneous differences between C and C++.

Basic Type Definitions

In C, one can group data into a struct like so.

struct Foo {
	int bar;
	char* baz;
};

In C, structs (and enums and unions) 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 typedefs 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 typedefed 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 typedefed.

typedef struct LinkedList {
	struct LinkedList* previous; // works
	LinkedList* next; // doesn't work
} LinkedList;

In C++, there is no separate namespace for struct/enum/unions; 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 friends, 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.

Function Overloading

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.

References

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:

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);
}

Object-Oriented Programming

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).

Functions on a Type

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.

Static Functions

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;
}

Constructors

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.

Destructors

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).

Templates

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;
}

Last updated: 2022-12-23 09:56:36 -0500.