Current mission: finish filling in the OTDs

SNCA:C++/Tutorial

From Soyjak Wiki, the free ensoyclopedia
Jump to navigationJump to search

The following is a brief tutorial on the functionality of C++. This tutorial assumes you have already read the C tutorial and familiar with the concepts it introduces.

This tutorial assumes the user has C++23 or later, as well as a library supporting import std;. If you don't have access to modules, you can easily find the corresponding documentation about which header to #include on a website like cppreference.com.

This tutorial also further assumes basic familiarity with programming.

Hello World[edit | edit source]

The Hello World program in C++ is as follows:

import std;

int main() {
    std::println("Hello, world!");
}

Here, import std; imports the entirety of the C++ standard library for use into the program. Unlike headers in C, import is purely semantic and handled at compile-time, whereas #include takes the contents of a header and inserts them at the site of inclusion at preprocessing time, which is slower to compile in most cases.

If you're familiar with Java (see the Java tutorial for details), you will notice that in C++, there is no restriction that all code must reside in a class, or a namespace for that matter.

The main function is the same as it is in C. In C++, it must always reside in the global namespace.

Finally, the function println() (from the std namespace) is a function that prints a string to the console, with an appended newline at the end. To avoid appending an extra newline, you can use print() instead. If you recall from the C tutorial, where we used printf(), you may ask why we don't use printf(). This is because C's printf() lacks type safety and is considered less flexible than C++'s print functions.

Variables[edit | edit source]

Variables in C++ work exactly as they do in C.

Also, to bring a class from the standard library into scope, you need to use a using statement, which imports that type from whatever namespace it resides in into the global scope so that it may be referred to without the namespace. You can also use using namespace, which imports all symbols from a namespace, into scope, however this is often discouraged due to often being unpredictable, and often adds unwanted symbols into scope.

When using headers, never put a using statement in a header as this transitively applies to anything that includes the header. However, it's okay to use using statements in modules as modules won't transitively pass using statements unless you explicitly export them.

Also, print functions support formatting. If you're familiar with Python, the syntax is the same.

In C++, there is a string class in the standard library, which is much simpler and versatile to use than the C char* form of strings.

import std;

using std::string;

int main() {
    int age = 18;
    string name = "Nate";
    string website = "soyjak.party";
    std::println("{} is {}, and thus old enough to post on {}!", age, name, website);
}

Reading input[edit | edit source]

There are many ways to read input, whether from a file or from the global input stream. To just read single values from the global input stream, use std::cin and pipe the values into the variables using the >> operator.

import std;

using std::cin;

int main() {
    int a;
    int b;
    std::print("Enter a first number: ");
    cin >> a;
    std::println();
    std::print("Enter a second number: ");
    cin >> b;
    std::println();
    std::println("The sum of the numbers is a + b = {} + {} = {}", a, b, a + b);
}

Meanwhile, if you want to read a full line, use the getline() function:

import std;

using std::cin;
using std::string;

int main() {
    string name;
    std::print("What's your name? ");
    std::getline(cin, name);
    std::println();
    std::println("{}? That's a gemmy name.", name);
}

Range-based for loop[edit | edit source]

Loops in C++ are exactly the same as in C. However, C++ also introduces the "range-based for loop", useful for iterating over an array or a collection. The syntax is for (element : collection) { body }.

import std;

using std::array;

int main() {
    array<int, 5> a = {1, 2, 3, 4, 5};
    for (int x : a) {
        std::println("x = {}", x);
    }
}

Namespaces and modules[edit | edit source]

A namespace is essentially a grouping of things, such as classes, functions, variables, etc. Unlike in Java, you can have as many namespaces inside a file as you want, and a primary benefit of namespaces is that they allow you to declare two things with the same name, as long as they reside in different namespaces.

import std;

namespace soy {
    void postBait() {
        std::println("Trans rights are human rights!");
    }
}

namespace pol {
    void postBait() {
        std::println("Israel is our greatest ally!");
    }
}

You can nest multiple namespaces inside each other:

namespace soy {
    namespace qa {
        // The function is soy::qa::wipeCatty()
        void wipeCatty() {
            std::println("GEEEEEEG, im flooding the catty with low-quality slop");
        }
    }

    namespace pol {
        // The function is soy::pol::trollBrownoids()
        void trollBrownoids() {
            std::println("Quote is racist crackkka who banned us from /soy/ o algo");
        }
    }
}

namespace cuck::lgbt {
    // The function is cuck::lgbt::postBait()
    void postBait() {
        std::println("You will never be a woman!");
    }
}

Meanwhile, a module is a whole file that can be imported. You have strict control over what you want to export from the module as well. In C++, you don't have to separate a module into "interface" and "implementation" like you do in C with header files/source files.

export module soy;

import std;

namespace soy {
    // This function is exported
    export void postBait() {
        std::println("Trans rights are human rights!");
    }

    // This function is not exported
    void postSlopjak() {
        std::println("Good morning saar, here is my newest slopjak");
    }
}

Then, it can be imported like so:

import soy;

int main() {
    soy::postBait(); // Prints "Trans rights are human rights!");

    // soy::postSlopjak();
    // This isn't possible, as soy::postSlopjak() isn't exported from module soy
}

Modules can themselves be broken up into partitions. Partitions can't be imported on their own, but belong to the module they are part of.

export module soyjakparty:Anon;

export namespace soy {
    class Anon {
        // ...
    };
}

It is not enforced, but one good way of organising things is making files, namespaces, modules all match each other (just like in Java).

References[edit | edit source]

In C++, a reference is basically like a pointer in C, with the following caveats:

  • A reference is declared with &, so for any type T, the reference to T is denoted T&.
  • References must be initialised immediately; for example, int& x; is illegal. References must also always point to a valid object, and cannot point to nullptr.
  • References cannot be pointed to another variable.

References are essentially like pointers, that dereference automatically.

They are particularly useful in functions, to avoid copying. Also, if you use a const reference (const T&), it prevents modification and avoids copying. This is usually the most efficient and safe way to do things.

import std;

using std::string;

void greet(const string& name) {
    std::println("Welcome to the sharty, {}, o algo.", name);
}

Classes[edit | edit source]

In C++, a class is basically the same as a C struct, except the fields are by default private. A class defines private, protected and public regions of code, where code inside the different regions have different access:

  • private code can be accessed only by that class. Most fields are usually private.
  • protected code can be accessed only by the class and its descendants (through inheritance).
  • public code can be accessed by anyone. Most things, such as methods, constructors and destructors are public.

It is important to define clear boundaries on what code is public and private, to preserve code encapsulation. This ensures only code that is intended to be able to modify classes may modify classes, while other code is prevented from doing so.

In C++, you no longer have to prefix a class/struct with class or struct to first - for example, what had to be referred to as struct X in C is now tolerated as just X in C++.

A class consists of a constructor, a destructor, methods and fields. The fields consist of the data the class holds, while methods are functions that act on that class.

For a class X, a constructor X() is a special function that is used to create an object. It is named the same as the class, and has no return type. You can have as many different constructors as you like. A destructor ~X(), however, is another special function that is used to destroy (deallocate the memory and release the resources) an object. Like the constructor, it is named the same as the class but prefixed with a tilde (~), and likewise has no return type either, but you can only have one destructor. Destructors never have parameters. Destructors typically are never manually called, but they are automatically called once either the object leaves scope or is manually deleted (explained in dynamic memory allocation).

A method of a class can be marked const, which guarantees that the method will never modify the data in the class. This allows the compiler to make optimisations based on the assumption the class is never modified.

export module soyjakparty.wiki.examples;

import std;

export namespace soyjakparty::wiki::examples {

class Nusoi {
private:
    string name;
    int numberOfYears;
public:
    Nusoi(const string& name, int numberOfYears):
        name{name}, numberOfYears{numberOfYears} {
        std::println("I'm a nusoi named {} and I've been on the bald men with glasses site for {} years");
    }

    ~Nusoi() {
        std::println("OYYYYY QUOTE DON'T 'NISH ME");
    }

    void participateInRaids() {
        std::println("OYYYY DOCTOS 'ox and 'ape this tranny!!!");
    }

    string getName() const {
        return name;
    }

    int getNumberOfYears() const {
        return numberOfYears;
    }
};

}

Then, the class can be used like so:

import soyjakparty.wiki.examples;

using soyjakparty::wiki::examples::Nusoi;

int main() {
    Nusoi nate = Nusoi("Nate Higgers", 1);
    nate.participateInRaids();
    int years = nate.getNumberOfYears();
}

Enums[edit | edit source]

In C++, the old C enum is still type-unsafe, but C++ supports a new enum, enum class, which is scoped and type-safe.

enum class Colour {
    RED,
    ORANGE,
    YELLOW,
    GREEN,
    BLUE,
    INDIGO,
    VIOLET
};

enum class Day {
    MONDAY = 1,
    TUESDAY = 2,
    WEDNESDAY = 3,
    THURSDAY = 4,
    FRIDAY = 5,
    SATURDAY = 6,
    SUNDAY = 7
};

Colour colour = Colour::RED;
Day day = Day::WEDNESDAY;

colour = Day::FRIDAY; // Not allowed
day = 15; // Not allowed

Inheritance[edit | edit source]

Classes can extend each other, allowing them to inherit the features of another class. The class that is being inheriting from is called the "base" class while the class that is inheriting is called the "derived" class.

C++ lacks "interfaces" like Java, but it can essentially emulate them using classes whose methods are all pure virtual, called an "abstract class". C++ does not limit the number of base classes a class may extend, unlike languages like Java and C# which limit it to one, but permit implementing any number of interfaces.

The keyword public denotes the access level of inheritance, denoting that a class inherits its base class's features, and can be used in places where its base class is expected. This is the most common form of inheritance used, and is equivalent to the inheritance used in other languages. private inheritance specifies that the class inherits the features of the base class, but cannot be used in place of its base class.

export module soyjakparty.wiki.examples;

import std;

export namespace soyjakparty::wiki::examples {

class Vehicle {
private:
    string brand;
    int year;
public:
    Vehicle(const string& brand, int year):
        brand{brand}, year{year} {}

    virtual ~Vehicle() = default;

    virtual void displayInfo() {
        std::println("Brand: {}, Year: {}", brand, year);
    }
};

class Flyable {
public:
    virtual void fly() = 0;
};

class Drivable {
    virtual void drive() = 0;
};

class Car : public Vehicle, public Drivable {
private:
    int doors;
public:
    Car(const string& brand, int year, int doors):
        Vehicle(brand, year), doors{doors} {}

    ~Car() override = default;

    void drive() override {
        std::println("Driving the car with {} doors.", doors);
    }

    void displayInfo() override {
        std::println("Brand: {}, Year: {}, Doors: {}", brand, year, doors);
    }
};

class Airplane : public Vehicle, public Flyable {
private:
    int maxAltitude;
public:
    Airplane(const string& brand, int year, int maxAltitude):
        Vehicle(brand, year), maxAltitude{maxAltitude} {}

    ~Airplane() override = default;

    void fly() override {
        std::println("Flying the airplane at max altitude of {} metres.", maxAltitude);
    }

    void displayInfo() override {
        std::println("Brand: {}, Year: {}, Max altitude: {}", brand, year, maxAltitude);
    }
};

}

In use:

import soyjakparty.wiki.examples;

using soyjakparty::wiki::examples::Airplane;
using soyjakparty::wiki::examples::Car;

int main() {
    // Use in-place initialisation instead
    Car car("Toyota", 2022, 4);
    Airplane airplane("Boeing", 2020, 35000);

    car.displayInfo();
    car.drive();

    airplane.displayInfo();
    airplane.fly();
}

Dynamic memory allocation[edit | edit source]

Using dynamic memory allocation, we can allocate memory for objects at runtime rather than at compile time. This declares the object in heap memory, rather than stack memory. Unlike Java, where all objects must be on the heap while primitives are on the stack, in C++ there is no such restriction; anything can be placed anywhere. This is often useful in situations where behaviour must depend on runtime information, such as user-provided information.

In C++, instead of C's malloc() and free() functions, we use the new and delete keywords. These are much better, as they don't require you to directly specify the size of the allocation.

To declare an object on the heap, use the new operator. This allocates a piece of heap memory for that object, and returns a pointer to that object. However, once you are done with that object you must de-allocate it with the delete operator. Failure to do so will result in the program leaking memory, which is a serious bug if the program continues to run for extended periods of time.

Consider the previous vehicle example again. Then, if using dynamic memory allocation:

import soyjakparty.wiki.examples;

using soyjakparty::wiki::examples::Airplane;
using soyjakparty::wiki::examples::Car;

int main() {
    // Use in-place initialisation instead
    Car* car = new Car("Toyota", 2022, 4);
    Airplane* airplane = new Airplane("Boeing", 2020, 35000);

    car.displayInfo();
    car.drive();

    airplane.displayInfo();
    airplane.fly();

    delete car;
    delete airplane;
}

One way to avoid leaking memory is by using smart pointers. Two of the most important smart pointers are:

  • unique_ptr<T>: a pointer that uniquely owns the object that it points to. Once this pointer goes out of scope, the object is disposed of. It is often created using a function make_unique().
  • shared_ptr<T>: a pointer that shares the object that it points to. It counts the number of references pointing to that object, and once that count reaches 0, it automatically disposes of the object. It is often created using a function make_shared().
import std;
import soyjakparty.wiki.examples;

using std::unique_ptr;

using soyjakparty::wiki::examples::Airplane;
using soyjakparty::wiki::examples::Car;

int main() {
    unique_ptr<Car> car = std::make_unique<Car>("Toyota", 2022, 4);
    unique_ptr<Airplane> airplane = std::make_unique<Airplane>("Boeing", 2020, 35000);

    car.displayInfo();
    car.drive();

    airplane.displayInfo();
    airplane.fly();
}

Exceptions[edit | edit source]

In C++, if you want to indicate an error, you can throw an exception. This is done with the throw keyword. When an exception is thrown, it travels up the call stack, and if it remains uncaught once it passes main(), then the program crashes. If you want to try some operations which may throw an exception, use the try block, and when an exception is thrown, the catch block will be used to catch the exception. It is best recommended to catch exceptions by const reference.

  • catch (const NusoiException& e) catches any exception that is of type NusoiException.
  • catch (const exception& e) catches any exception that is has exception as a base class.
  • catch (const E& e) catches any object that is an E. In C++, any object, even non-exception types, can be thrown.
  • catch (...) catches any thrown object, regardless of type.

The standard library provides various exception types. These include:

  • exception: the usual base class for an exception.
  • logic_error: represents a logic error in programming.
  • runtime_error: represents an error that happens at runtime.
  • invalid_argument: represents an error when an argument is not accepted.
  • domain_error: represents an error when an input may be outside a domain of defined operation.
  • length_error: represents an error when something exceeds an implementation-defined length.
  • out_of_range: represents an error when something tries to access an element of a collection (such as a string or vector) beyond its valid range of indexes.
  • overflow_error: represents an arithmetic overflow error.
  • underflow_error: represents an arithmetic underflow error.
  • system_error: represents an error when something with operating system facilities fails.

These exceptions all have a method what() that gives the error message associated with the exception.

import std;

using std::cerr;
using std::exception;
using std::runtime_error;
using std::string;

class NusoiException: public runtime_error {
    explicit NusoiException(const string& message):
        runtime_error(message) {}
};

class Nusoi {
    // The implementation from earlier

    void doxThisTarget(const string& targetName) {
        if (targetName == this->name) {
            throw NusoiException("OYYYY i can't 'ox this target, that's me!");
        } else {
            std::println("Hey /raid/, let's all 'ox this target!");
        }
    }
};

int main() {
    try {
        Nusoi nate("Nate Higgers", 1);
        nate.participateInRaids();
        nate.doxThisTarget("Troonella Ackermann");
        nate.doxThisTarget("Nate Higgers"); // throws
    } catch (const NusoiException& e) {
        std::println(cerr, "Caught NusoiException: {}", e.what());
    } catch (const exception& e) {
        std::println(cerr, "Caught exception: {}", e.what());
    } catch (...) {
        std::println(cerr, "Caught an unknown exception.");
    }
}

A function can also be marked as noexcept, indicating that it will never throw. This allows the compiler to make optimisations to the function based on the assumption that it does not throw, and crashes the program if an exception passes the stack frame of a noexcept function.

class SoyBooru {
private:
    int postCount;
public:
    // ...

    int getPostCount() const noexcept {
        return postCount;
    }
};

Templates[edit | edit source]

In C++, a template allows you to specialise a function or class for a different type without having to manually create overloads.

template <typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

template <typename T>
class LinkedList {
private:
    T current;
    LinkedList<T>* next;
public:
    LinkedList(T current, LinkedList<T>* next = nullptr):
        current{current}, next{next} {}

    T getCurrent() {
        return current;
    }

    LinkedList<T>* getRest() {
        return next;
    }
};

By using concepts, templates can be constrained to satisfy conditions.

import std;

using std::same_as;
using std::floating_point;

template <typename T>
concept Drawable = requires (T t) {
    { t.draw() } -> same_as<void>; // T::draw must return void
    { t.area() } -> floating_point; // matches any floating-point type
};

// The class Circle satisfies the Drawable concept
// Circle does not need to declare it satisfies Drawable, however
class Circle {
private:
    const double radius;
public:
    explicit Circle(double r):
        radius{r} {}

    void draw() const {
        // draw a circle...
    }

    double area() const noexcept {
        return std::numbers::pi * radius * radius;
    }
};

// Only types which satisfy Drawable may be used
template <Drawable T>
void render(const T& shape) {
    // ...
    shape.draw();
    std::println("Drew shape with area {}", shape.area());
}

Standard library types[edit | edit source]

The C++ standard library contains many useful classes, all within the std namespace.

General use[edit | edit source]

Some of the most fundamental or universally usable classes include:

  • any is a class which may store any type. It is basically a type-erased container.
  • optional<T> represents either an object (of type T) or an absence of the object. If there is nothing in optional, its value is nullopt.
  • expected<T, E> represents either an object (of type T) or an error (of type E).
  • pair<T, U> is a type representing a pair of objects, one of type T and the other of type U.
  • tuple<Ts...> is a type representing a tuple, which can hold an indefinite number of objects of any type.
  • variant<Ts...> is a type representing a type-safe union, which can contain one of several types.

Arrays and collections[edit | edit source]

C++ features various collection (container) types. Some of the most important ones are:

  • array<T, N> is an array of length N storing objects of type T. The length N is fixed and must be specified at compile-time. It is essentially a wrapper over the C array T[], providing object-oriented features.
  • vector<T> is essentially a dynamic array storing objects of type T. Objects can be freely inserted and removed from the vector.
  • unordered_map<K, V> is a dictionary-like type storing keys of type K, and mapping them to objects of type V.
  • span<T> represents a non-owning view over piece of contiguous memory, such as an array.

There are more, but they aren't particularly worth talking about (yet).