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

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:

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.