In this article, more basic concepts of C++ (version 17) are covered.
typedef
and using
, while new (basic) ones can be created with enum
.
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.typedef int arr3[3];
for an array containing three int
values or typedef int* intp;
for a pointer to int
objects.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.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.
typedef
, nor using
declare a new data type, just an alternative name for an existent one.
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;
.
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;} |
#include <iostream> #include <new> //For nothrow int main() { int arr_size; std::cout << "Enter array size." << std::endl; std::cin >> arr_size; //arr_size unknown at compilation time. nothrow instead of exception handler. int *arr = new (std::nothrow) int[arr_size]; if (arr == nullptr) { std::cerr << "Failed to allocate memory."; return 0; } for (int i = 0; i < arr_size; i++) { //Fill array with squares and print it to console. arr[i] = i * i; std::cout << arr[i] << " "; } delete arr; //Free allocated memory space. return 0; }Exemplary output:
Enter array size. 10 0 1 4 9 16 25 36 49 64 81
std::terminate()
is called, ending the program prematurely. To avoid this, exception handlers are used.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). 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)
. ...
is used as the input argument, the handler catches all exceptions. 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
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). 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
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.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.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
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.