Feature guide

High-level introduction to design of the Magnum library and basic building blocks.

Before you continue further, here are the essential bits of knowledge to help you around.

Library organization and naming scheme

The Magnum project consists of a library with core functionality and optional sub-libraries. Each library is in its own namespace, which corresponds to a sub-folder on the include path – so e.g. things from Magnum::Math are included from Magnum/Math/. Magnum builds upon Corrade, which provides platform abstraction, basic utilities and containers.

To reduce redundant verbosity, references throughout the documentation and code in example snippets have the root Magnum and Corrade namespaces omitted. In other words, as if the following was done:

using namespace Corrade;
using namespace Magnum;

If Magnum is the primary library you're building your project on, it's recommended that you do the same — we're careful to not pollute the root namespace with overly generic names. However, except for Literals namespaces, using the subnamespaces is not recommended, as naming in those is deliberately picked short and may introduce conflicts (such as Containers::String vs Utility::String or GL::PixelFormat vs Vk::PixelFormat).

If you frown upon the thought of using namespace Magnum yet don't want to wear your fingers off by repeatedly writing Magnum:: everywhere, a common shorthand that projects go with is the following — similarly in spirit to import numpy as np:

namespace Cr = Corrade;
namespace Mn = Magnum;

Include files and forward declarations

Depending on where you come from, the #include policy used by Magnum might either make you happy or freak you out. In short, there's no "include the world" file, and instead you're supposed to include dedicated headers for APIs you want to use. The main reason is compile times, and the speed gain from doing it this way is too great to be ignored.

The general rule is that each top-level class has a corresponding include, so for example Math::Matrix4 is included as Magnum/Math/Matrix4.h. It's not always like that though, and to help you out, documentation of each namespace and class tells you what to include, and if particular namespace members are defined in different headers, then detailed docs of each has a corresponding #include directive listed as well, ready to be copied.

In order to have the includes actually independent of each other, most Magnum types are forward-declared, and where possible, the header only relies on forward declarations. Which means that often the type already exists as a declaration, and in order to actually use it, you have to include the concrete definition of it. If you don't, the compiler will complain about use of an incomplete type. For example:

#include <Magnum/Magnum.h>        /* only a Matrix4 forward declaration */
#include <Magnum/Math/Matrix4.h>  /* the actual definition */

Matrix4 a = Matrix4::translation({3.0f, 1.0f, 0.5f});

Of course not all headers can be written with just forward declarations, so there still are some transitive dependencies between headers (for example, the Magnum/Math/Matrix4.h header pulls in Magnum/Math/Vector4.h as well). But for the most part this is an implementation detail and as such shouldn't be relied on.

For happy compile times you're encouraged to rely on forward declarations in your code as well. See Forward declarations instead of includes for more information.

Debug output

One of the essential debugging workflows is inspection of variable contents by printing them out. Magnum defines a lot of new math types, enums and containers and it would be very painful if you had to loop over their contents or perform manual enum-to-string conversion every time you want to see what's inside.

Because writing to standard output and printing values for debugging purposes are distinct use cases with potentially conflicting requirements (should an enum value get written as a number? or as a name? a fully qualified name?), Magnum doesn't provide operator<< overloads for std::ostream.

Instead, there's Utility::Debug (and its variants, Utility::Warning, Utility::Error and Utility::Fatal), for your typing convenience also aliased to just Debug / Warning / Error directly in the Magnum namespace. On its own it's able to print builtin types, all usual C++ containers as well as their Corrade equivalents. Magnum then implements debug printers for its own math types, basic structures and most enums, which get printed as fully-qualified names. For example:

Image2D image = ;

Debug{} << "Image format is" << image.format() << "and size" << image.size();
Debug{} << "Color of the first pixel is" << image.pixels<Color4ub>()[0][0];

Image format is PixelFormat::RGBA8Srgb and size Vector(256, 192)
Color of the bottom-left pixel is #33b27f

The main goal of this utility is convenience and readability — values are implicitly delimited by spaces and ended with a newline, container contents written with commas etc. Check out the class documentation for advanced features like colors, output redirection or printing file/line info.

Error handling in Magnum

Magnum differentiates between two kinds of errors — a programmer error (for example an out-of-bounds access) and a runtime error (for example when a file can't be opened). Programmer errors are assertions, directly leading to a program abort. Runtime errors expect you to do the error handling, and if you don't do that, then you're likely to hit an assertion — a programmer error — shortly after.

The assertions are deliberately not recoverable and are by default kept in release builds as well. A lot of effort is put into assertion messages, which clearly show you what went wrong. They get always printed to the standard output or its equivalent, see the troubleshooting guide for ways how to retrieve it on various platforms.

To avoid polluting user code with excessive error handling, runtime errors are restricted only where strictly necessary, and always documented as such. For performance and portability reasons, exceptions aren't used, Instead, the errors usually manifest as an invalid return value (for example an empty Containers::Optional) and a diagnostic message accompanying the error is printed via Utility::Error. If needed, you can optionally redirect the message to a string or a log file.

Learn by example

Before you do a deep dive into the documentation, and if you haven't done already, it's recommended to go through the Getting Started Guide and check out at least the first example:

Learn through documentation

Each of the following pages provides a high-level description of a certain area of the library. It's recommended to read through these first to understand the overall principles and only then go to documentation of each concrete class and function.