Some parts of C++ require parts of the C++ Standard Library. We’ve lightly touched on classes like std::initializer_list and std::typeinfo already, but today we’ll look at a whole lot more. We’ll see parts of the Standard Library that would typically be built into the language or are otherwise strongly tied to making use of particular language features.

Table of Contents

Source Location

Let’s start with an easy one that was just added in C++20: <source_location>. Right away we see how the naming convention of the C++ Standard Library differs from the C Standard Library and it’s C++ wrappers. The C Standard Library header file name would likely be abbreviated into something like srcloc.h. The C++ wrapper would then be named csrcloc. The C++ Standard Library usually prefers to spell out names more verbosely in snake_case and without any extension, .h or otherwise.

Within the <source_location> header file we see the naming convention continue with the source_location class. There isn’t always a 1:1 mapping, but snake_case is almost always used. The source_location class is placed in the std namespace, so we typically talk about it as std::source_location. The std namespace is reserved for the C++ Standard Library.

Now to the actual purpose of std::source_location. As its name suggests, it provides a facility for expressing a location in the source code. It has copy and move constructors, but no way for us to create one from scratch. Instead, we call its static member function current and one is returned:

#include <source_location>
 
void Foo()
{
    std::source_location sl = std::source_location::current();
    DebugLog(sl.line()); // 42
    DebugLog(sl.column()); // 61
    DebugLog(sl.file_name()); // example.cpp
    DebugLog(sl.function_name()); // void Foo()
}

The file_name member function provides a replacement for the __FILE__ macro. Likewise, line replaces __LINE__. We also get column and function_name which aren’t present in standardized macro form. In C#, the StackTrace and StackFrame classes are roughly equivalent to source_location.

It’s worth noting here at the start that a lot of code will include a using statement to remove the need to type std:: over and over. It’s a namespace like any other, so we have all the normal options. For example, a using namespace std; at the file level right after the #include lines is common:

#include <source_location>
using namespace std;
 
void Foo()
{
    source_location sl = source_location::current();
    DebugLog(sl.line()); // 43
    DebugLog(sl.column()); // 61
    DebugLog(sl.file_name()); // example.cpp
    DebugLog(sl.function_name()); // void Foo()
}

To avoid bringing the entire Standard Library into scope, we might using just particular classes:

#include <source_location>
using std::source_location;
 
void Foo()
{
    source_location sl = source_location::current();
    DebugLog(sl.line()); // 43
    DebugLog(sl.column()); // 61
    DebugLog(sl.file_name()); // example.cpp
    DebugLog(sl.function_name()); // void Foo()
}

Or we might put the using just where the Standard Library is being used:

#include <source_location>
 
void Foo()
{
    using namespace std;
    source_location sl = source_location::current();
    DebugLog(sl.line()); // 43
    DebugLog(sl.column()); // 61
    DebugLog(sl.file_name()); // example.cpp
    DebugLog(sl.function_name()); // void Foo()
}

All of these are commonly seen in C++ codebases and provide good options for removing a lot of the std:: clutter. There is, however, one bad option which should be avoided: adding using at the top level of header files. Because header files are essentially copied and pasted into other files via #include, these using statements introduce the Standard Library to name lookup for all the files that #include them. When header files #include other header files, this impact extends even further:

// top.h
#include <source_location>
using namespace std; // Bad idea
 
// middlea.h
#include "top.h" // Pastes "using namespace std;" here
 
// middleb.h
#include "top.h" // Pastes "using namespace std;" here
 
// bottoma.cpp
#include "middlea.h" // Pastes "using namespace std;" here
 
// bottomb.cpp
#include "middlea.h" // Pastes "using namespace std;" here
 
// bottomc.cpp
#include "middleb.h" // Pastes "using namespace std;" here
 
// bottomd.cpp
#include "middled.h" // Pastes "using namespace std;" here

The effects of using namespace std; in top.h have spread to the files that #include it: middlea.h and middleb.h. That then spreads to the files that #include those: bottoma.cpp, bottomb.cpp, bottomc.cpp, and bottomd.cpp. It’s best to avoid this to so as to not undo the compartmentalization that namespaces provide and instead let individual files choose when and where they want to breach it:

// top.h
#include <source_location>
struct SourceLocationPrinter
{
    static void Print()
    {
        // OK: only applies to this function, not files that #include
        using namespace std;
 
        source_location sl = source_location::current();
        DebugLog(sl.line()); // 43
        DebugLog(sl.column()); // 61
        DebugLog(sl.file_name()); // example.cpp
        DebugLog(sl.function_name()); // void SourceLocationPrinter::Print()
    }
};
 
// middlea.h
#include "top.h" // Does not paste "using namespace std;" here
 
// middleb.h
#include "top.h" // Does not paste "using namespace std;" here
Initializer List

Next up let’s look at <initializer_list>. We touched on std::initializer_list before, but now we’ll take a closer look. An instance of this class template is automatically created and passed to the constructor when we use braced list initialization:

struct AssetLoader
{
    AssetLoader(std::initializer_list<const char*> paths)
    {
        for (const char* path : paths)
        {
            DebugLog(path);
        }
    }
};
 
AssetLoader loader = {
    "/path/to/model",
    "/path/to/texture",
    "/path/to/audioclip"
};

We could rewrite this to create the std::initializer_list<const char*> manually, but this relies on that same braced list initialization as std::initializer_list doesn’t have any direct way to create an empty instance:

AssetLoader loader(std::initializer_list<const char*>{
    "/path/to/model",
    "/path/to/texture",
    "/path/to/audioclip"
});

As we see in the AssetLoader constructor, range-based for loops work with std::initializer_list. There’s also a size member function, but there’s no index operator so we can’t use a typical for loop:

AssetLoader(std::initializer_list<const char*> paths)
{
    // OK: there's a size member function
    for (size_t i = 0; i < paths.size(); ++i)
    {
        // Compiler error: no operator[int]
        DebugLog(paths[i]);
    }
}

The C# equivalent is to take a params managed array. The compiler builds that managed array for us at the call site like how a std::initializer_list is built for us.

Type Info and Index

We’ve also seen a little bit of <typeinfo> when looking at RTTI. When we use typeid, we get back a std::type_info which is like a lightweight version of the C# Type class:

#include <typeinfo>
 
struct Vector2
{
    float X;
    float Y;
};
 
struct Vector3
{
    float X;
    float Y;
    float Z;
};
 
Vector2 v2{ 2, 4 };
Vector3 v3{ 2, 4, 6 };
 
// All constructors are deleted, but we can still get a reference
const std::type_info& ti2 = typeid(v2);
const std::type_info& ti3 = typeid(v3);
 
// There are only three public members
// They are all implementation-specific
DebugLog(ti2.name()); // Maybe struct Vector2
DebugLog(ti2.hash_code()); // Maybe 3282828341814375180
DebugLog(ti2.before(ti3)); // Maybe true

Relatedly, <typeinfo> defines the bad_typeid class that’s thrown as an exception when trying to take the typeid of a null pointer to a polymorphic class. In C# we’d get a NullReferenceException instead of this when we try to write nullObj.GetType():

#include <typeinfo>
 
struct Vector2
{
    float X;
    float Y;
 
    // A virtual function makes this class polymorphic
    virtual bool IsNearlyZero(float epsilonSq)
    {
        return abs(X*X + Y*Y) < epsilonSq;
    }
};
 
void Foo()
{
    Vector2* pVec = nullptr;
    try
    {
        // Try to take typeid of a null pointer to a polymorphic class
        DebugLog(typeid(*pVec).name());
    }
    // This particular exception is thrown
    catch (const std::bad_typeid& e)
    {
        DebugLog(e.what()); // Maybe "Attempted a typeid of nullptr pointer!"
    }
}

There’s also a bad_cast class that’s thrown when we try to dynamic_cast two unrelated types. This is the equivalent of the C# InvalidCastException class:

#include <typeinfo>
 
struct Vector2
{
    float X;
    float Y;
 
    virtual bool IsNearlyZero(float epsilonSq)
    {
        return abs(X*X + Y*Y) < epsilonSq;
    }
};
 
struct Vector3
{
    float X;
    float Y;
    float Z;
 
    virtual bool IsNearlyZero(float epsilonSq)
    {
        return abs(X*X + Y*Y + Z*Z) < epsilonSq;
    }
};
 
void Foo()
{
    Vector3 vec3{};
    try
    {
        Vector2& vec2 = dynamic_cast<Vector2&>(vec3);
    }
    catch (const std::bad_cast& e)
    {
        DebugLog(e.what()); // Maybe "Bad dynamic_cast!"!
    }
}

The <typeindex> header provides the std::type_index class, not an integer, which wraps the std::type_info we saw above. This class provides some overloaded operators so we can compare them in various ways, not just with the before member function:

#include <typeindex>
 
struct Vector2
{
    float X;
    float Y;
};
 
struct Vector3
{
    float X;
    float Y;
    float Z;
};
 
Vector2 v2{ 2, 4 };
Vector3 v3{ 2, 4, 6 };
 
// Pass a std::type_info to the constructor
const std::type_index ti2{ typeid(v2) };
const std::type_index ti3{ typeid(v3) };
 
// Some member functions from std::type_info carry over
DebugLog(ti2.name()); // Maybe struct Vector2
DebugLog(ti2.hash_code()); // Maybe 3282828341814375180
 
// Overloaded operators are provided for comparison
DebugLog(ti2 == ti3); // false
DebugLog(ti2 < ti3); // Maybe true
DebugLog(ti2 > ti3); // Maybe false

The C# Type class can’t be compared directly, so we’d instead compare something like its fully-qualified name string.

Compare

C++20 introduced the three-way comparison operator: x <=> y. This allows us to overload one operator stating how our class compares to another class. We need return an object that supports all of the individual comparison operators: <, <=, >, >=, ==, and !=. Rather than defining our own class to do that, the Standard Library provides some built-in classes via the <compare> header. For example, we can return a std::strong_ordering via one of its static data members:

#include <compare>
 
struct Integer
{
    int Value;
 
    std::strong_ordering operator<=>(const Integer& other) const
    {
        // Determine the relationship once
        return Value < other.Value ?
            std::strong_ordering::less :
            Value > other.Value ?
                std::strong_ordering::greater :
                std::strong_ordering::equal;
    }
};
 
Integer one{ 1 };
Integer two{ 2 };
std::strong_ordering oneVsTwo = one <=> two;
 
// All the individual comparison operators are supported
DebugLog(oneVsTwo < 0); // true
DebugLog(oneVsTwo <= 0); // true
DebugLog(oneVsTwo > 0); // false
DebugLog(oneVsTwo >= 0); // false
DebugLog(oneVsTwo == 0); // false
DebugLog(oneVsTwo != 0); // true

There are similar classes for weaker comparison results: std::weak_ordering and std::partial_ordering. There are also helper functions that call all of these operators on any of these comparison classes so we can write this instead:

DebugLog(std::is_lt(oneVsTwo)); // true
DebugLog(std::is_lteq(oneVsTwo)); // true
DebugLog(std::is_gt(oneVsTwo)); // false
DebugLog(std::is_gteq(oneVsTwo)); // false
DebugLog(std::is_eq(oneVsTwo)); // false
DebugLog(std::is_neq(oneVsTwo)); // true

Helper functions are provided to get these ordering class objects, even from primitives:

std::strong_ordering so = std::strong_order(1, 2);
std::weak_ordering wo = std::weak_order(1, 2);
std::partial_ordering po = std::partial_order(1, 2);
std::strong_ordering sof = std::compare_strong_order_fallback(1, 2);
std::weak_ordering wof = std::compare_weak_order_fallback(1, 2);
std::partial_ordering pof = std::compare_partial_order_fallback(1, 2);

There’s no equivalent to the <=> operator in C#, so there’s no equivalent to this header.

Concepts

Another C++20 feature with library support is concepts. A whole host of pre-defined concepts are available for our immediate use and for us to extend. Here are a few of them:

#include <concepts>
 
template <typename T1, typename T2>
requires std::same_as<T1, T2>
bool SameAs;
 
template <typename T>
requires std::integral<T>
bool Integral;
 
template <typename T>
requires std::default_initializable<T>
bool DefaultInitializable;
 
SameAs<int, int>; // OK
SameAs<int, float>; // Compiler error
 
Integral<int>; // OK
Integral<float>; // Compiler error
 
struct NoDefaultCtor { NoDefaultCtor() = delete; };
DefaultInitializable<int>; // OK
DefaultInitializable<NoDefaultCtor>; // Compiler error

There are many more available for diverse needs: derived_from, destructible, equality_comparable, copyable, invocable, and so forth. None of these have a C# counterpart as C# generic constraints are not customizable.

Coroutine

The final C++20 feature receiving library support is coroutine. The <coroutine> header provides the required std::coroutine_handle class we’ve already seen when implementing our own coroutine “return objects.” It also provides std::suspend_never and std::suspend_always so we don’t have to write our own versions as we did before. Here’s how our trivial coroutine example would have looked with std::suspend_never instead of our custom NeverSuspend class:

#include <coroutine>
 
struct ReturnObj
{
    ReturnObj()
    {
        DebugLog("ReturnObj ctor");
    }
 
    ~ReturnObj()
    {
        DebugLog("ReturnObj dtor");
    }
 
    struct promise_type
    {
        promise_type()
        {
            DebugLog("promise_type ctor");
        }
 
        ~promise_type()
        {
            DebugLog("promise_type dtor");
        }
 
        ReturnObj get_return_object()
        {
            DebugLog("promise_type::get_return_object");
            return ReturnObj{};
        }
 
        std::suspend_never initial_suspend()
        {
            DebugLog("promise_type::initial_suspend");
            return std::suspend_never{};
        }
 
        void return_void()
        {
            DebugLog("promise_type::return_void");
        }
 
        std::suspend_never final_suspend()
        {
            DebugLog("promise_type::final_suspend");
            return std::suspend_never{};
        }
 
        void unhandled_exception()
        {
            DebugLog("promise_type unhandled_exception");
        }
    };
};
 
ReturnObj SimpleCoroutine()
{
    DebugLog("Start of coroutine");
    co_return;
    DebugLog("End of coroutine");
}
 
void Foo()
{
    DebugLog("Calling coroutine");
    ReturnObj ret = SimpleCoroutine();
    DebugLog("Done");
}

Here’s what this prints:

Calling coroutine
promise_type ctor
promise_type::get_return_object
ReturnObj ctor
promise_type::initial_suspend
Start of coroutine
promise_type::return_void
promise_type::final_suspend
promise_type dtor
Done
ReturnObj dtor

There’s also a trio of no-op coroutine features: std::noop_coroutine, std::noop_coroutine_promise, and std::noop_coroutine_handle. These implement the coroutine equivalent of a void noop() {} function. noop_coroutine is the coroutine and it returns a noop_coroutine_handle whose “promise” is a noop_coroutine_promise.

C# doesn’t provide this level of customization for its iterator functions, but we can implement IEnumerable, IEnumerable<T>, IEnumerator, and IEnumerator<T> to take some control over iteration. Those interfaces and their methods provide the closest analog to this header file.

Version

As we saw when looking at the preprocessor, a <version> header exists with a ton of macros we can use to check if various features are available in the language and Standard Library. For example, we can check for some of the Standard Library features we’ve seen today:

#include <version>
 
// These print "true" or "false" depending on whether the Standard Library has
// these features available
DebugLog("Standard Library concepts?", __cplusplus >= __cpp_lib_concepts);
DebugLog("source_location?", __cplusplus >= __cpp_lib_source_location);

C# has a handful of standardized preprocessor symbols, including DEBUG and TRACE, but its suite is nowhere near as extensive as in C++. Each .NET implementation, such as Unity and .NET Core, may define its own additional symbols, such as UNITY_2020_2_OR_NEWER and these version numbers are often correlated to available language and library features.

Type Traits

Finally for today we have <type_traits> which is used for compile-time programming. This header predates concepts in C++20, so a lot of it overlaps in a non-concept form. For example, we have various constexpr variable templates that check whether types fulfill certain criteria. These are available as static member variables of class templates and as namespace-scope variable templates:

#include <type_traits>
 
// Use a static value data member of a class template
static_assert(std::is_integral<int>::value); // OK
static_assert(std::is_integral<float>::value); // Compiler error
 
// Use a variable template
static_assert(std::is_integral_v<int>); // OK
static_assert(std::is_integral_v<float>); // Compiler error

There are tons of these available and they can check for nearly any feature of a type. Here are some more advanced ones:

#include <type_traits>
 
struct Vector2
{
    float X;
    float Y;
};
 
struct Player
{
    int Score;
 
    Player(const Player& other)
    {
        Score = other.Score;
    }
};
 
static_assert(std::is_bounded_array_v<int[3]>); // OK
static_assert(std::is_bounded_array_v<int[]>); // Compiler error
 
static_assert(std::is_trivially_copyable_v<Vector2>); // OK
static_assert(std::is_trivially_copyable_v<Player>); // Compiler error

Besides type checks, there are various utilities for querying types:

#include <type_traits>
 
DebugLog(std::rank_v<int[10]>); // 1
DebugLog(std::rank_v<int[10][20]>); // 2
DebugLog(std::extent_v<int[10][20], 0>); // 10
DebugLog(std::extent_v<int[10][20], 1>); // 20
DebugLog(std::alignment_of_v<float>); // Maybe 4
DebugLog(std::alignment_of_v<double>); // Maybe 8

We can also get modified versions of types:

#include <type_traits>
 
// We know T is a pointer (e.g. int*)
// We don't have a name for what it points to (e.g. int)
// Use std::remove_pointer_t to get it
template <typename T>
auto Dereference(T ptr) -> std::remove_pointer_t<T>
{
    return *ptr;
}
 
int x = 123;
int* p = &x;
int result = Dereference(p);
DebugLog(result); // 123

One particularly useful function is std::underlying_type which can be used to implement safe “cast” functions to and from enumerations:

#include <type_traits>
 
// "Cast" from an integer to an enum
template <typename TEnum, typename TInt>
TEnum FromInteger(TInt i)
{
    // Make sure the template parameters are an enum and an integer
    static_assert(std::is_enum_v<TEnum>);
    static_assert(std::is_integral_v<TInt>);
 
    // Use is_same_v from type_traits to ensure that TInt is the underlying type
    // of TEnum
    static_assert(std::is_same_v<std::underlying_type_t<TEnum>, TInt>);
 
    // Perform the cast
    return static_cast<TEnum>(i);
}
 
// "Cast" from an enum to an integer
template <typename TEnum>
auto ToInteger(TEnum e) -> std::underlying_type_t<TEnum>
{
    // Make sure the template parameter is an enum
    static_assert(std::is_enum_v<TEnum>);
 
    // Perform the cast
    return static_cast<std::underlying_type_t<TEnum>>(e);
}
 
enum class Color : uint64_t
{
    Red,
    Green,
    Blue
};
 
Color c = Color::Green;
DebugLog(c); // Green
 
// Cast from enum to integer
auto i = ToInteger(c);
DebugLog(i); // 1
 
// Cast from integer to enum
Color c2 = FromInteger<Color>(i);
DebugLog(c2); // Green

These “cast” functions imply no runtime overhead as all the checks occur at compile time. They do, however, add safety since we’ll get a compiler diagnostic if we accidentally try to use the wrong size of type:

// Compiler error: short is not the underlying type
FromInteger<Color>(uint16_t{ 1 });
 
// Compiler warning: target integer type is too small
// The "treat warnings as errors" setting can be used to turn this into an error
uint16_t i = ToInteger(c);

Some of this functionality exists in C# via the Type class and its related reflection classes: FieldInfo, PropertyInfo, etc. In contrast to C++, these all execute at runtime where their C++ counterparts execute at compile time.

Conclusion

Some parts of C++ rely on the Standard Library. We need to use std::initializer_list to handle braced list initialization and we need to use std::coroutine_handle to implement coroutine return objects. This is similar to C# that enshrines parts of the .NET API into the language: Type, System.Single, etc.

Today we’ve seen a lot of those quasi-language features as well as some general language support functionality like source_location and a lot of pre-defined concepts. These are foundational elements of the language and library, but also give a taste of what’s to come in terms of the Standard Library’s design.