Knowledge Dump

C++ Basics 2

In this article, more basic concepts of C++ (version 17) are covered.

Contents

Custom Data Types
Existing data types can be customized with an alternative name, by applying the keywords typedef and using, while new (basic) ones can be created with enum.
typedef / using
In order to give some data type an additional name (type alias), the keyword typedef can be applied. Example: typedef int new_int; declares the alternative name new_int for the fundamental data type int. After the declaration, int and new_int can be used interchangeably.
Type aliases also work for arrays and pointers, e.g. typedef int arr3[3]; for an array containing three int values or typedef int* intp; for a pointer to int objects.
Since C++11, the keyword using offers the same functionality with a slightly altered syntax: using new_int = int;. However, unlike typedef, using can also be applied to define template aliases, i.e. new names for template classes, without the need to specify the template parameter.
Example:
template <class T>
struct Foo {
	T value;
};

typedef Foo<int> Foo1;		//Define type alias "Foo1" for Foo<int>.
using Foo2 = Foo<int>;		//Define type alias "Foo2" for Foo<int>.

// template <class T> 
// typedef Foo<T> Foo3;		//Doesn't work! Can't create template aliases with typedef.

template <class T>
using Foo4 = Foo<T>;		//Defines template alias with unspecified template parameter T.


int main()
{
	Foo1 obj1;		//Declare objects of type Foo<int>.
	Foo2 obj2;		//No template parameter required in the declaration, since it's included in the type alias.

	Foo4<float> obj4;	//Declare object of type Foo<float>. 
				//Since Foo4 is template alias, the template parameter needs to be specified in the declaration.

	return 0;
}
Aliases are usually used to shorten long expressions, or as a placeholder for a data type that might have to be changed in the future. In the latter case, one saves time by only having to rewrite the alias declaration, instead of every occurrence in the code.
It should also be noted that neither typedef, nor using declare a new data type, just an alternative name for an existent one.
enum
Data types with limited amount of possible values can be created with the enum keyword. Example: enum color{red, blue, green, yellow}; declares the type color, which can be used to create objects as usual: color obj1 = red;. As indicated by the keyword, the new data types are just enumerations of possible values with custom identifiers. Each value corresponds to an unsigned integer (which is not necessarily unique to it), starting with 0 for the first value by default and incremented by 1 for each value. Other integers can be assigned in the declaration. Example:
#include <iostream>

enum Color { red, blue, green, orange, yellow = 7, brown };	//Define data type Color with possible values red, blue,...
								//The corresponding integer values are 0,1,2,3,7,8.

int main()
{
	Color obj1 = blue;		//Define Color object with value blue / 1.
	Color obj2 = green;		//Define Color object with value green / 2.
	std::cout << obj1 + obj2;	//blue + green = 3
	std::cout << color::yellow;	//yellow corresponding to 7.

	return 0;
}
If implicit conversion to int is not desired, the keyword enum class can be used in the declaration. Besides that the syntax remains the same, with the only difference being that the scope resolution operator needs to be used, when defining objects, e.g. Color obj1 = Color::blue;.

Memory Allocation
There are three ways memory is allocated for an object: static, dynamic(heap) or stack allocation. Each of the three offers a different lifetime and potential scope for the object:
Type Description Lifetime Example
static Any object that is declared in the global environment, a namespace or with the static keyword. Size fixed and known at compilation time. From declaration until program end. static int a;
dynamic / heap Objects allocated with the operator new or the C functions malloc, calloc, realloc. Memory has to be freed manually with delete (delete[] for arrays) or free, unless smart pointers are used. Size possibly unknown until runtime. If allocation fails, e.g. due to a lack of memory space, a bad_alloc exception is thrown. From allocation until deletion. int *a = new int; delete a;
stack All objects declared in a local scope are allocated on the stack and deallocated upon leaving it. From declaration until end of scope. {int a;}
Each allocation type has a different use case:
  • static: Objects of fixed size that need to persist for the whole duration of the program runtime.
  • dynamic: Large objects and objects of possibly unknown size during compilation.
  • stack: Quick short-term allocation for smaller objects, which are only required in a local scope.
Short example for dynamic memory allocation with unknown allocation size at compilation time:


Exceptions and Exception Handlers
Exceptions may be thrown during compilation, or when something goes wrong during the runtime of a program. When they are not caught, the function std::terminate() is called, ending the program prematurely. To avoid this, exception handlers are used.

To produce exceptions, the keyword throw is used, followed by an object of any data type. In order to be able to react to them, they have to be inside of a try{} environment. When an exception is thrown there, the rest of the code inside the try block is skipped and the program jumps to the subsequent exception handler(s).
Exception handlers are defined using the keyword catch, followed by any data type in parentheses and (a set of) statements in curly braces. When an identifier is included inside the parentheses, the thrown exception's value is copied there. It is usually preferred to avoid this copying by using references, e.g. catch(int &a).
Multiple handlers can be used, each catching a different data type. When ... is used as the input argument, the handler catches all exceptions.
Example:
try {
	//...some code...
	{throw 1; }	//If the exception is thrown, subsequent code is skipped. Throws integer 1.
	//...some code...
	{throw 2; }	//Throws integer 2, if previous exception hasn't been triggered already.
	//...some code...
	{throw 'c'; }	//Throws character.
}
//Exception handler for int
catch (int &a) {	//a has numeric value of thrown int exceptions.
	if (a == 1) {
		//Do sth. ...
	}
	if (a == 2) {
		//Do sth. else ...
	}
}
//Exception handler for any other type, besides int.
catch (...) {
	//Do sth. here...
}
When a try block inside a try environment is used, an inner exception handler can proceed to throw an exception to the handlers of the outer block, by calling throw; without an argument. Example:
try {
	try {
		throw 123;
	}
	catch (int &a) {
		throw;	//Pass exception on to upper handler.
		}
	}
	catch (...) {
			//This handler is skipped.
	}
}
catch (int &a) {
	//Catches exception from inner block.
}
exception Class
Many functions from the standard library throw exceptions, when problems are encountered. To catch and read those, the class exception from the equally named header can be used. Since the thrown exceptions are derived classes from the base class exception, they have to be caught by reference, to be able to read their error type. Example:
#include <iostream>
#include <exception>

int main()
{
	try {
		int *a = new int[100000000000000];	//Throws object of derived exception class std::bad_alloc.
	}
	catch (std::exception& ex) {			//Catches any object of class exception or classes derived from it.
		std::cerr << ex.what();			//Will print std::bad_alloc to console.
	}

	return 0;
}
When an exception object is caught, its subclass can be identified by calling the what() member function, which returns a C string containing information about it (e.g. it's name).
New classes for custom exceptions can be easily derived and only should redeclare the member function what(), which is also the only one besides constructors and destructor. Example:
#include <iostream>
#include <exception>

class new_ex1 : public std::exception {			//Declare new subclass of exception.
	virtual const char* what() const noexcept {	//Redefine virtual function what() for subclass.
		return "This is exception new_ex1.";	//Describe the exception. 
	}
};

int main()
{
	try {
		new_ex1 ex_obj;
		throw ex_obj;			//Throws object of derived exception class new_ex1.
	}
	catch (std::exception& ex) {		//Catch with reference, so that the exception type isn't lost.
		std::cerr << ex.what();		//Will print "This is exception new_ex1." to console.
	}

	return 0;
}
noexcept
The noexcept specifier can be used to mark functions, which are not expected to throw exceptions. It is simply added after the function declaration, e.g. void foo() noexcept; and can also be written as noexcept(true). Its purpose is twofold: Some functions, e.g. the standard library function std::move_if_noexcept, may require guarantees that a function doesn't throw exceptions. Besides this, it also serves as a compiler directive, which is possibly useful for optimization.
Contrarily, a function can also be explicitly marked as "possibly throwing", when adding noexcept(false). Since this is the default for most functions, it won't do anything in most cases. Notable exceptions are destructors, default (copy/move) constructors, copy/move assignment operators and deallocation functions, which are assumed to be non-throwing by default.
Generally noexcept specifiers should be used in a context, where they are likely going to be required by another function and it's very unlikely that the function will be changed to one that possibly throws exceptions.

Return Value Optimization
Return value optimization (RVO) is a special form of copy elision, used by most modern compilers. It may be applied, when a function returns a temporary/unnamed object, which subsequently would've been moved/copied to another object. In that case, the temporary object is instead constructed right into the destination object. This may cause the program to behave differently than expected, particularly when the constructors/destructor of the object have side effects. Note that even when RVO is used and the copy/move constructors aren't called, they still have to be defined for the program to compile.
In some cases, RVO also happens for named returns (named RVO). To qualify, the object must be returned by value and can't be part of the function parameters. Similarly, multiple return statements (e.g. due to the use of control statements) with different returned objects prevent RVO. Example:
#include <iostream>

class class1 {	//Class prints message when any constructor/assignment/destructor was called.
public:
	class1() { std::cout << "Constructor called." << std::endl; }
	~class1() { std::cout << "Destructor called." << std::endl; }
	class1(const class1&) { std::cout << "Copy constructor called." << std::endl; }
	class1(class1&&) { std::cout << "Move constructor called." << std::endl; }
	class1& operator=(const class1&) { std::cout << "Copy assignment operator called." << std::endl; }
	class1& operator=(class1&&) { std::cout << "Move assignment operator called." << std::endl; }
};

class1 foo() {
	class1 obj;
	return obj;	//Named object returned by value.
}

class1 bar() {
	class1 obj1, obj2;		//Constructs two objects.
	if (true) { return obj1; }	//Obviously, obj1 will be returned all the time.
	return obj2;			//However, the presence of two differing return "possibilities" is enough to prevent RVO.
}


int main()
{
	{class1 obj = class1(); }	//RVO used, no move constructor required. 1 Constructor, 1 Destructor call.
	std::cout << std::endl;
	{class1 obj = foo(); }		//NRVO instead of move constructor. 1 Constructor, 1 Destructor call.
	std::cout << std::endl;
	{class1 obj = bar(); }		//No NRVO. Calls move constructor, due to multiple return statements.

	return 0;
}
Output: (with copy elision)
Constructor called.
Destructor called.

Constructor called.
Destructor called.

Constructor called.
Constructor called.
Move constructor called.
Destructor called.
Destructor called.
Destructor called.
Output: (with disabled copy elision)
Constructor called.
Move constructor called.
Destructor called.
Destructor called.

Constructor called.
Move constructor called.
Destructor called.
Move constructor called.
Destructor called.
Destructor called.

Constructor called.
Constructor called.
Move constructor called.
Destructor called.
Destructor called.
Move constructor called.
Destructor called.
Destructor called.
Using the gcc compiler, copy elision can be disabled with the -fno-elide-constructors flag.