Sane C++ Libraries
C++ Platform Abstraction Libraries
Foundation

🟩 Primitive types, asserts, limits, Function, Span, Result, Tagged Union

Foundation library provides many fundamental type definitions and types widely used by other libraries.
As this is included and needed by almost every other library, it tries to keep bloat to the bare minimum.
Detailed documentation is in the Foundation topic.

Features

Classes

Class Description
SC::Span View over a contiguous sequence of items (pointer + size in elements).
SC::Result An ascii string used as boolean result. SC_TRY macro forwards errors to caller.
SC::Function Wraps function pointers, member functions and lambdas without ever allocating.
SC::Deferred Executes a function at end of current scope (in the spirit of Zig defer keyword).
SC::OpaqueObject Hides implementation details from public headers (static PIMPL).
SC::TaggedUnion Type safe union with an enum type, where each type has an associated enum value.
SC::UniqueHandle Move only handle that has a special tag value flagging its invalid state.

Macros

  • Compiler Macros: Compiler Macros Preprocessor macros to detect compiler and platform features.

Type Traits

  • Type Traits: Type Traits (EnableIf, AddPointer, RemovePointer, etc.)

Utilities

Class Description
SC::Assert Functions and macros to assert, exit() or abort() and capture backtraces.
SC::AlignedStorage A buffer of bytes with given alignment.
SC::MaxValue An object that can be converted to any primitive type providing its max value.
SC::Memory Centralized functions to allocate, reallocate and deallocate memory.

Status

🟩 Usable
The library is very simple it it has what is needed so far by the other libraries.

Description

There is an hard rule in the library Principles not to include system and compiler headers in public headers.
Foundation provides all primitive types to be used in headers and classes like SC::UniqueHandle, SC::OpaqueObject, SC::AlignedStorage to encourage static PIMPL in order to hide platform specific implementation details everywhere.

Function

Wraps function pointers, member functions and lambdas without ever allocating.
Example:

struct MyClass
{
float memberValue = 2.0;
int memberFunc(float a) { return static_cast<int>(a + memberValue); }
};
int someFunc(float a) { return static_cast<int>(a * 2); }
struct BigClass
{
uint64_t values[4];
};
// ... somewhere later
MyClass myClass;
Function<int(float)> func;
func = &someFunc; // Bind free func
func.bind<MyClass, &MyClass::memberFunc>(myClass); // Bind member func
func = [](float a) -> int { return static_cast<int>(a + 1.5); }; // Bind lambda func
BigClass bigClass;
// This will static_assert because sizeof(BigClass) (grabbed by copy) exceeds LAMBDA_SIZE
// func = [bigClass](float a) -> int { return static_cast<int>(a);};
unsigned long long uint64_t
Platform independent (8) bytes unsigned int.
Definition: PrimitiveTypes.h:42

Size of lambdas or less than LAMBDA_SIZE (currently 2 * sizeof(void*)).
If lambda is bigger than LAMBDA_SIZE the constructor will static assert.

Template Parameters
FuncTypeType of function to be wrapped (Lambda, free function or pointer to member function)

Deferred

Executes a function at end of current scope (in the spirit of Zig defer keyword).

Template Parameters
FThe lambda / function to execute

Example:

HANDLE processHandle = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_DUP_HANDLE, FALSE, processId);
if (processHandle == nullptr)
{
return false;
}
auto deferDeleteProcessHandle = SC::MakeDeferred(
[&] // Function (or lambda) that will be invoked at the end of scope
{
CloseHandle(processHandle);
processHandle = nullptr;
});
// Use processHandle that will be disposed at end of scope by the Deferred
// ...
// Deferred can be disarmed, meaning that the dispose function will not be executed
deferDeleteProcessHandle.disarm()
Deferred< F > MakeDeferred(F &&f)
Creates a Deferred object holding a function that will be invoked at end of current scope.
Definition: Deferred.h:61

TaggedUnion

Type safe union with an enum type, where each type has an associated enum value.

Template Parameters
Unionwith FieldTypes = TypeList<TaggedType<EnumType, EnumValue, Type>, ...>

Example:

namespace SC
{
struct TaggedUnionTest;
// Create an arbitrary enumeration with some values
enum TestType
{
TypeString = 10,
TypeInt = 110,
};
// Create the union definition containing a FieldTypes nested type
struct TestUnion
{
// Helper to save some typing
template <TestType E, typename T>
using Tag = TaggedType<TestType, E, T>;
// FieldsTypes MUST be defined to be a TypeList of TaggedType(s)
using FieldsTypes = TypeTraits::TypeList< // List all TargetType associations
Tag<TypeString, String>, // Associate TypeString with String
Tag<TypeInt, int>>; // Associate TypeInt with init
};
void taggedUnionUsageSnippet(Console& console)
{
// Create the tagged union on the TestUnion definition
TaggedUnion<TestUnion> test; // default initialized to first type (String)
// Access / Change type
String* ptr = test.field<TypeString>();
if (ptr) // If TypeString is not active type, ptr will be == nullptr
{
*ptr = "SomeValue";
}
test.changeTo<TypeInt>() = 2; // Change active type to TypeInt (compile time known)
// Switch on currently active type (TypeInt)
switch (test.getType())
{
case TypeString: console.print("String = {}", *test.field<TypeString>()); break;
case TypeInt: console.print("Int = {}", *test.field<TypeInt>()); break;
}
// Set current active type at runtime back to TypeString
test.setType(TypeString);
*test.field<TypeString>() = "Some new string";
}
} // namespace SC

OpaqueObject

Hides implementation details from public headers (static PIMPL).
Opaque object avoids the heap allocation that often comes with PIMPL, allowing to hide OS specific details from public headers. User declares size in bytes of a struct in the header but the structure can be defined in an implementation .cpp file. Choosing a size that is too small will generate a static_assert that contains in the error message the minimum size to use. Up to 4 functions will need to be defined to avoid linker errors (construct, destruct, moveConstruct, moveAssign). These functions are meant to be defined in a .cpp file that will know how to construct Object, as it can see its definition.

Template Parameters
DefinitionPass in a custom Definition declaring Sizes and alignment on different platforms

Example:

// ... in the header file
struct TestObject
{
private:
struct Internal;
// Define maximum sizes and alignment in bytes of Internal object on each platform
struct InternalDefinition
{
static constexpr int Windows = 224;
static constexpr int Apple = 144;
static constexpr int Default = sizeof(void*);
static constexpr size_t Alignment = alignof(void*);
using Object = Internal;
};
public:
// Must be public to avoid GCC complaining
using InternalOpaque = OpaqueObject<InternalDefinition>;
private:
InternalOpaque internal;
};
// ... In .cpp file
// Declare Internal struct with all platform specific details
struct SC::TestObject::Internal
{
SC_NtSetInformationFile pNtSetInformationFile = nullptr;
LPFN_CONNECTEX pConnectEx = nullptr;
LPFN_ACCEPTEX pAcceptEx = nullptr;
LPFN_DISCONNECTEX pDisconnectEx = nullptr;
// ... additional stuff
};
// Declare only two of the four functions to avoid linker errors.
template <>
void SC::TestObject::InternalOpaque::construct(Handle& buffer)
{
placementNew(buffer.reinterpret_as<Object>());
}
template <>
void SC::TestObject::InternalOpaque::destruct(Object& obj)
{
obj.~Object();
}

UniqueHandle

Move only handle that has a special tag value flagging its invalid state.
Typically used to wrap Operating System specific handles.

Template Parameters
DefinitionA struct declaring handle type, release function and invalid handle value.

Example:

// ... definition in header
struct PosixFileDescriptorDefinition
{
using Handle = int; // fd
static Result releaseHandle(Handle& handle);
static constexpr Handle Invalid = -1; // invalid fd
};
using PosixFileDescriptor = UniqueHandle<PosixFileDescriptorDefinition>;
// ...declaration in .cpp file
Result PosixFileDescriptorDefinition::releaseHandle(Handle& handle)
{
if (::close(handle) != 0)
{
return Result::Error("releaseHandle - close failed");
}
return Result(true);
}
// ... usage
PosixFileDescriptor myDescriptor;
const int nativeFd = ::open(filePath.getNullTerminatedNative(), flags, access);
// Assign the native handle to UniqueHandle (will release the existing one, if any)
myDescriptor.assign(nativeFd);
// UniqueHandle can only be moved, but not copied
PosixFileDescriptor otherDescriptor = move(myDescriptor);
// PosixFileDescriptor otherDescriptor = myDescriptor; // <- Doesn't compile
// Explicitly close (or it will be automatically released on scope close / destructor)
otherDescriptor.close()
// If detach() is called, the handle will be made invalid without releasing it
otherDescriptor.detach()
// Check handle for validity
if(otherDescriptor.isValid())
{
// ... do something
}
constexpr T && move(T &value)
Converts an lvalue to an rvalue reference.
Definition: Compiler.h:269

Roadmap

🟦 Complete Features:

  • Things will be added as needed

💡 Unplanned Features:

  • SharedPtr
  • UniquePtr
Note
In Principles there is a rule that discourages allocations of large number of tiny objects and also creating systems with unclear or shared memory ownership. For this reason this library is missing Smart Pointers.