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!