C++ For C# Developers: Part 37 – Missing Language Features
We’ve covered all the features in the C++ language! Still, C# has some features that are missing from C++. Today we’ll look at those and explore some alternatives to fill these gaps.
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
Fixed Statements
In an unsafe
context in C#, we can use fixed
statements to prevent the GC from moving an object:
// Unsafe function creates an unsafe context for its body unsafe void ZeroBytes(byte[] bytes) { // Prevent moving the array fixed (byte* pBytes = bytes) { // Access the array via a pointer for (int i = 0; i < bytes.Length; ++i) { pBytes[i] = 0; } } }
Since C++ has no GC, our objects never move around. We therefore have no need for a fixed
statement as we can simply take the address of objects:
struct ByteArray { int32_t Length; uint8_t* Bytes; }; void ZeroBytes(ByteArray& bytes) { ByteArray* pBytes = &bytes; for (int i = 0; i < pBytes->Length; ++i) { pBytes->Bytes[i] = 0; } }
There’s often no reason to bother with taking a pointer though. This is because objects are passed by value by default. In the above example, we take a ByteArray&
lvalue reference in ZeroBytes
because taking just a ByteArray
would cause a copy to be made when calling the function. So we normally already have a pointer-like reference to objects and can simply use it directly:
void ZeroBytes(ByteArray& bytes) { for (int i = 0; i < bytes.Length; ++i) { bytes.Bytes[i] = 0; } }
Fixed Size Buffers
Another meaning of fixed
in C# is to create a buffer of primitives (bool
, byte
, char
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, float
, or double
) that is directly part of a class
or struct
rather than a managed reference as we’d get with an array such as byte[]
:
// An unsafe context is required unsafe struct FixedLengthArray { // 16 integers are directly part of the struct // This is _not_ a managed reference to an int[] public fixed int Elements[16]; }
C++ has no need for fixed size buffers as it directly supports arrays:
struct FixedLengthArray { // 16 integers are directly part of the struct int32_t Elements[16]; };
Further, there’s no restriction to only use primitive types. Any type may be used:
struct Vector2 { float X; float Y; }; struct FixedLengthArray { // 16 Vector2s are directly part of the struct Vector2 Elements[16]; };
Properties
C# structs and classes support a special kind of function called “properties” that give the illusion that the user is referencing a field rather than calling a function:
class Player { // Conventionally called the "backing field" string m_Name; // Property called Name of type string public string Name { // Its "get" function takes no parameters and must return the property // type: string get { return m_Name; } // The "set" function is implicitly passed a single parameter of the // property type (string) and must return void set { m_Name = value; } } } Player p = new Player(); // Call "set" on the Name property and pass "Jackson" as the value parameter p.Name = "Jackson"; // Call "get" on the Name property and get the returned string DebugLog(p.Name);
When the bodies of the get
and set
functions and the “backing field” are trivial, as shown above, automatically-implemented properties can be used to tell the compiler to generate this boilerplate:
class Player { public string Name { get; set; } }
C++ doesn’t have properties. Instead, naming conventions are typically used to create pairs of “get” and “set” functions. Here’s a popular naming convention:
struct Player { const char* m_Name; const char* GetName() const { return m_Name; } void SetName(const char* value) { m_Name = value; } }; Player p{}; p.SetName("Jackson"); DebugLog(p.GetName());
Another popular convention relies on overloading to eliminate the “Get” and “Set” prefixes:
struct Player { const char* m_Name; const char* Name() const { return m_Name; } void Name(const char* value) { m_Name = value; } }; Player p{}; p.Name("Jackson"); DebugLog(p.Name());
Whichever convention is chosen, macros can be used to remove the boilerplate:
// Macro to create a property #define AUTO_PROPERTY(propType, propName) \ propType m_##propName; \ const propType& propName() const \ { \ return m_##propName; \ } \ void propName(const propType& value) \ { \ m_##propName = value; \ } struct Player { // Create the property AUTO_PROPERTY(const char*, Name) }; Player p{}; p.Name("Jackson"); DebugLog(p.Name());
Extern
To call functions implemented outside of the .NET environment, C# can declare them as extern
. Typically this is used to call into C or C++ code:
using System.Runtime.InteropServices; public static class WindowsApi { // This function is implemented in Windows' User32.dll [DllImport("User32.dll", CharSet=CharSet.Unicode)] public static extern int MessageBox( IntPtr handle, string message, string caption, int type); } // Call the external function WindowsApi.MessageBox((IntPtr)0, "Hello!", "Title", 0);
The extern
keyword in C++ has a different meaning: implemented in another translation unit. To call functions in another DLL, we use our platform’s API to load the DLL, call functions, then unload it. Here’s how we can do that with the Windows API:
// Platform API that provides DLL access #include <windows.h> // Load the DLL auto dll = LoadLibraryA("User32.dll"); // Get the address of the MessageBoxA function (ASCII version of MessageBox) auto proc = GetProcAddress(dll, "MessageBoxA"); // Cast to the appropriate kind of function pointer auto mb = (int32_t(*)(void*, const char*, const char*, uint32_t))(proc); // Call MessageBoxA via the function pointer (*mb)(nullptr, "Hello!", "Title", 0); // Unload the DLL FreeLibrary(dll);
The .NET environment takes care of loading and unloading DLLs referenced by [DllImport]
as well as creating function pointers to the associated extern
functions. As a trade-off, we lose control over elements of the process such as timing and error handling.
For multi-platform C++ code, it’s typical to wrap this platform-specific functionality in an abstraction layer that uses the preprocessor to make the right calls. For example:
/////////////// // platform.hpp /////////////// // Windows #ifdef _WIN32 #include <windows.h> // Non-Windows (e.g. macOS) #else // TODO #endif class Platform { #if _WIN32 using MessageBoxFuncPtr = int32_t(*)( void*, const char*, const char*, uint32_t); HMODULE dll; MessageBoxFuncPtr mb; #else // TODO #endif public: Platform() { #if _WIN32 dll = LoadLibraryA("User32.dll"); mb = (MessageBoxFuncPtr)(GetProcAddress(dll, "MessageBoxA")); #else // TODO #endif } ~Platform() { #if _WIN32 FreeLibrary(dll); #else // TODO #endif } // Abstracts calls to MessageBoxA on Windows and something else on other // platforms (e.g. macOS) void MessageBox(const char* message, const char* title) { #if _WIN32 (*mb)(nullptr, message, title, 0); #else // TODO #endif } }; /////////// // game.cpp /////////// #include "platform.hpp" Platform platform{}; platform.MessageBox("Hello!", "Title");
One alternative to using preprocessor directives like this is to create different files per platform: platform_windows.cpp
, platform_macos.cpp
, etc. Each contains an implementation of the Platform
class with code appropriate for the platform it’s intended to be compiled for. The project can then be configured to only compile one of these files so there will be no link time conflict as only one Platform
class will exist.
Extension Methods
C# gives the illusion that we can add methods to classes and structs. These are not really added though as they are still static
functions outside the class or struct. C# just allows for them to be called on instances of the class or struct they “extend”:
public static class ArrayExtensions { // Extension method on float[] because the first parameter has "this" public static float Average(this float[] array) { float sum = 0; foreach (float cur in array) { sum += cur; } return sum / array.Length; } } float[] array = { 1, 2, 3 }; // Call the extension method like it's a method of float[] DebugLog(array.Average()); // 2 // Or call it normally DebugLog(ArrayExtensions.Average(array));
The first version (array.Average()
) is rewritten by the compiler into the second version (ArrayExtensions.Average(array)
). Extension methods don’t get any special access to the class or struct they contain. For example, they can’t access private
fields.
The C++ version of this is similar to the second version: we typically write a “free function” outside of any class that takes the class to “extend” as a parameter:
float Average(float* array, int32_t length) { float sum = 0; for (int32_t i = 0; i < length; ++i) { sum += array[i]; } return sum / length; } float array[] = { 1, 2, 3 }; DebugLog(Average(array, 3)); // 2
Functions like this could be put into a namespace or made into static member functions of a class, but the principal remains: the function is disconnected from what it “extends” with no special access to it.
Checked Arithmetic
C# features the checked
keyword to perform runtime checks on arithmetic. We can opt into this on a per-expression basis or for a whole block:
public class Player { public uint Health; public void TakeDamage(uint amount) { // Opt into arithmetic checking checked { // If this underflows, an OverflowException is thrown Health -= amount; } } } Player p = new Player{ Health = 100 }; // OK: Health is now 50 p.TakeDamage(50); // OverflowException: tried to underflow Health to -20 p.TakeDamage(70);
C++ doesn’t have built-in arithmetic checking. Instead, we have a few options. First, we can perform our own manual arithmetic checks:
struct OverflowException { }; struct Player { uint32_t Health; void TakeDamage(uint32_t amount) { if (amount > Health) { throw OverflowException{}; } Health -= amount; } }; Player p{ 100 }; // OK: Health is now 50 p.TakeDamage(50); // OverflowException: tried to underflow Health to -20 p.TakeDamage(70);
Second, we can wrap numeric types in structs and overload operators with the checks. This option is the closest match to checked
blocks in C# as it allows us to perform checks on many operations without needing to write anything for each operation:
struct CheckedUint32 { uint32_t Value; // Conversion from uint32_t CheckedUint32(uint32_t value) : Value(value) { } // Overload the subtraction operator to check for underflow CheckedUint32 operator-(uint32_t amount) { if (amount > Value) { throw OverflowException{}; } return Value - amount; } // Implicit conversion back to uint32_t operator uint32_t() { return Value; } }; struct Player { uint32_t Health; void TakeDamage(uint32_t amount) { // Put Health in a wrapper struct to check its arithmetic operators Health = CheckedUint32{ Health } - amount; } };
Or we can create functions that perform checks. This is a close match to checked
expressions in C# that apply only to one operation:
uint32_t CheckedSubtraction(uint32_t a, uint32_t b) { if (b > a) { throw OverflowException{}; } return a - b; } struct Player { uint32_t Health; void TakeDamage(uint32_t amount) { Health = CheckedSubtraction(Health, amount); } };
This last approach is taken by libraries such as Boost Checked Arithmetic.
The unchecked
keyword isn’t present in C++ because there’s no checked
arithmetic to disable.
Nameof
C#’s nameof
operator gets a string
name of a variable, type, or member:
Player p = new Player(); DebugLog(nameof(p)); // p
C++ doesn’t have this feature built in, but there’s a library available that provides a NAMEOF
macro for similar functionality:
Player p{}; DebugLog(NAMEOF(p)); // p
As with the C# operator, it supports variables, types, and members. Additionally, it supports macros, enum “flag” values, and operates at both compile time and run time.
Decimal
C# has a built-in decimal
type for financial calculations and other times where decimal places need to be represented without any rounding:
float f = 1.0f; for (int i = 0; i < 10; ++i) { f -= 0.1f; DebugLog(f); }
This prints inaccurate values because floating point can’t represent these without rounding:
0.9 0.8 0.6999999 0.5999999 0.4999999 0.3999999 0.2999999 0.1999999 0.09999993 -7.450581E-08
If we use decimal
, we avoid the rounding:
decimal d = 1.0m; for (int i = 0; i < 10; ++i) { d -= 0.1m; DebugLog(d); }
This prints:
0.9 0.8 0.7 0.6 0.5 0.4 0.3 0.2 0.1 0.0
C++ doesn’t have a built-in decimal
type, but libraries such as GMP and decimal_for_cpp create such types. For example, in the latter library we can write this:
#include "decimal.h" using namespace dec; decimal<1> d{ 1.0 }; for (int i = 0; i < 10; ++i) { d -= decimal<1>{ 0.1 }; DebugLog(d); }
This prints what we’d expect:
0.9 0.8 0.7 0.6 0.5 0.4 0.3 0.2 0.1 0.0
Reflection
C# implicitly stores a lot of information about the structure of the program in the binaries it compiles to. This information is then accessible at runtime for the C# code to query via “reflection” methods like GetType
that return classes like Type
.
public class Player { public string Name; public uint Health; } Player p = new Player{Name="Jackson", Health=100}; Type type = p.GetType(); foreach (FieldInfo fi in type.GetFields()) { DebugLog(fi.Name + ": " + fi.GetValue(p)); }
This prints:
Name: Jackson Health: 100
The only information like this that C++ stores is data for RTTI to support dynamic_cast
and typeid
. It’s a very small subset of what’s available in C# since even full type names are not usually preserved in typeid
and only classes with virtual functions are supported by dynamic_cast
.
So if we want to store this information, we need to store it ourselves. We could do this manually by implementing our own reflection system:
// Different types our reflection system supports enum class Type { None, ConstCharPointer, Uint32, }; // Reflected values struct Value { // Type of the value Type Type; // Pointer to the value void* ValuePtr; }; // "Interface" to "implement" to make a class support reflection struct IReflectable { using MemberName = const char*; // Get names of the class' fields virtual const MemberName* GetFieldNames() = 0; // Get a value of a class instance's field virtual Value GetFieldValue(MemberName* name) = 0; }; // Player supports reflection class Player : IReflectable { // Names of the fields. Initialized after the class. static const char* const FieldNames[3]; public: const char* Name; uint32_t Health; virtual const MemberName* GetFieldNames() override { return FieldNames; } virtual Value GetFieldValue(MemberName* name) override { // strcmp is a Standard Library function returning 0 when strings equal if (!strcmp(name, "Name")) { return { Type::ConstCharPointer, &Name }; } else if (!strcmp(name, "Health")) { return { Type::Uint32, &Health }; } return { Type::None, nullptr }; } }; const char* const Player::FieldNames[3]{ "Name", "Health", nullptr }; Player p; p.Name = "Jackson"; p.Health = 100; auto fieldNames = p.GetFieldNames(); for (int32_t i = 0; fieldNames[i]; ++i) { auto fieldName = fieldNames[i]; auto fieldValue = p.GetFieldValue(fieldName); switch (fieldValue.Type) { case Type::ConstCharPointer: DebugLog(fieldName, ": ", *(const char**)fieldValue.ValuePtr); break; case Type::Uint32: DebugLog(fieldName, ": ", *(uint32_t*)fieldValue.ValuePtr); break; } }
This prints the same logs:
Name: Jackson Health: 100
Manually adding all of this is quite tedious and creates a maintenance problem as the code changes. As a result, there are many reflection libraries available for C++ to remove a lot of the boilerplate:
- Boost PFR provides basic reflection
- Magic Enum supports only enums
- RTTR has more complete reflection features
For example, in RTTR we can write just this:
#include <rttr/registration> using namespace rttr; class Player { const char* Name; uint32_t Health; }; RTTR_REGISTRATION { registration::class_<Player>("Player") .property("Name", &Player::Name) .property("Health", &Player::Health); } Player p; p.Name = "Jackson"; p.Health = 100; type t = type::get<Player>(); for (auto& prop : t.get_properties()) { DebugLog(prop.get_name(), ": ", prop.get_value(p)); }
Conclusion
Neither language is a subset of the other. In almost every article of this series, we’ve seen how the C++ version of various language features is larger and more powerful than the C# equivalent. Today we’ve seen the opposite: several features that C# has that C++ doesn’t.
We’ve also seen how to at least approximate that functionality in C++ when it’s desired. Sometimes, as in the case of fixed
statements and buffers, there’s no need for such a feature in C++ and we can simply stop using the C# feature.
Other times, as with extension methods and properties, there’s no direct equivalent and we’ll need to tweak our design to fit C++ norms such as the use of free functions and “GetX” functions.
Then there are some cases where libraries are available to implement similar functionality on top of the C++ language. This is the case with decimal
, nameof
, and reflection. The powerful, relatively low-level tools that C++ provides makes the effecient implementation of such libraries possible.
Finally, there are some missing C# features whose alternatives depend on the Standard Library specifically. We’ll see those alternatives later on in the series.
#1 by Mike D on January 25th, 2021 ·
Thank you, I learned a lot from this series. I actually started in C++, but until now I used it more like C with classes :-)
#2 by christo161 on February 22nd, 2021 ·
is there such an operator in cpp like ||= or &&= in angular?
https://www.facebook.com/1962137414116297/posts/2681186165544748/
#3 by jackson on February 24th, 2021 ·
No, but you can create functions that do something similar by using references:
#4 by christo161 on February 22nd, 2021 ·
Jason Turner made an episode also about missing features in cpp:
https://youtu.be/RpSjU2C5SYc