C++ For C# Developers: Part 39 – Language Support Library
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
- Part 1: Introduction
- Part 2: Primitive Types and Literals
- Part 3: Variables and Initialization
- Part 4: Functions
- Part 5: Build Model
- Part 6: Control Flow
- Part 7: Pointers, Arrays, and Strings
- Part 8: References
- Part 9: Enumerations
- Part 10: Struct Basics
- Part 11: Struct Functions
- Part 12: Constructors and Destructors
- Part 13: Initialization
- Part 14: Inheritance
- Part 15: Struct and Class Permissions
- Part 16: Struct and Class Wrap-up
- Part 17: Namespaces
- Part 18: Exceptions
- Part 19: Dynamic Allocation
- Part 20: Implicit Type Conversion
- Part 21: Casting and RTTI
- Part 22: Lambdas
- Part 23: Compile-Time Programming
- Part 24: Preprocessor
- Part 25: Intro to Templates
- Part 26: Template Parameters
- Part 27: Template Deduction and Specialization
- Part 28: Variadic Templates
- Part 29: Template Constraints
- Part 30: Type Aliases
- Part 31: Deconstructing and Attributes
- Part 32: Thread-Local Storage and Volatile
- Part 33: Alignment, Assembly, and Language Linkage
- Part 34: Fold Expressions and Elaborated Type Specifiers
- Part 35: Modules, The New Build Model
- Part 36: Coroutines
- Part 37: Missing Language Features
- Part 38: C Standard Library
- Part 39: Language Support Library
- Part 40: Utilities Library
- Part 41: System Integration Library
- Part 42: Numbers Library
- Part 43: Threading Library
- Part 44: Strings Library
- Part 45: Array Containers Library
- Part 46: Other Containers Library
- Part 47: Containers Library Wrapup
- Part 48: Algorithms Library
- Part 49: Ranges and Parallel Algorithms
- Part 50: I/O Library
- Part 51: Missing Library Features
- Part 52: Idioms and Best Practices
- Part 53: Conclusion
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.
#1 by typoman on March 28th, 2021 ·
typo: DefaultInitialable (x3)
#2 by jackson on March 28th, 2021 ·
Fixed. Thanks!
#3 by Mikant on March 28th, 2021 ·
“std::initializer_list doesn’t have any direct way to create a non-empty instance” Didn’t you mean “an empty”?
#4 by jackson on March 28th, 2021 ·
I think they’re equivalent, but I updated the article anyways since I like your version better. Thanks!