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

🟩 Primitive types, asserts, limits, Function, Span, Result, Buffer

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::Buffer An heap allocated byte buffer that can optionally use an inline buffer.
SC::Span View over a contiguous sequence of items (pointer + size in elements).
SC::SpanString An writable view over an ASCII string (to avoid including Strings library)
SC::SpanStringView An read-only view over an ASCII string (to avoid including Strings library)
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::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.

Buffer

An heap allocated byte buffer that can optionally use an inline buffer.

See also
SC::SmallBuffer to use an inline buffer that can optionally become heap allocated as needed.
Note
This class (and SC::SmallBuffer) reduces needs for the header-only SC::Vector (from Containers). SC::Buffer avoids some compile time / executable size bloat because it's not header only.

Example:

bool funcRequiringBuffer(Buffer& buffer)
{
for (size_t idx = 0; idx < buffer.size(); ++idx)
{
if (buffer[idx] != 123)
return false;
}
return true;
}
void BufferTest::basic()
{
Buffer buffer;
// Allocate 16 bytes
SC_TEST_EXPECT(buffer.resizeWithoutInitializing(16));
// Buffer is not inline (it's heap allocated)
SC_TEST_EXPECT(not buffer.isInlineBuffer());
// Fill buffer with a value
buffer.clear();
SC_TEST_EXPECT(buffer.resize(buffer.capacity(), 123));
funcRequiringBuffer(buffer);
// Declare a buffer with inline capacity of 128 bytes
SmallBuffer<128> smallBuffer;
// copy buffer (will not allocate)
smallBuffer = buffer;
// smallBuffer is using inline buffer (no heap allocation)
SC_TEST_EXPECT(smallBuffer.isInlineBuffer());
SC_TEST_EXPECT(smallBuffer.size() == 16);
SC_TEST_EXPECT(smallBuffer.capacity() == 128);
// SmallBuffer can be passed in place of regular Buffer
funcRequiringBuffer(smallBuffer);
SC_TEST_EXPECT(buffer.resizeWithoutInitializing(1024));
// SmallBuffer now will allocate 1024 bytes
// by using assignCopy instead of assignment operator
// caller can check for allocation failure
SC_TEST_EXPECT(smallBuffer.assign(buffer.toSpanConst()));
SC_TEST_EXPECT(not smallBuffer.isInlineBuffer());
SC_TEST_EXPECT(smallBuffer.size() == 1024);
SC_TEST_EXPECT(smallBuffer.capacity() == 1024);
// Allocate 2kb on another buffer
Buffer buffer2;
SC_TEST_EXPECT(buffer2.resizeWithoutInitializing(2048));
// SmallBuffer will "steal" the 2Kb buffer
smallBuffer = move(buffer2);
SC_TEST_EXPECT(smallBuffer.size() == 2048);
SC_TEST_EXPECT(smallBuffer.capacity() == 2048);
SC_TEST_EXPECT(buffer2.isEmpty());
// Resize small buffer to its original capacity
SC_TEST_EXPECT(smallBuffer.resizeWithoutInitializing(128));
// The heap block is still in use
SC_TEST_EXPECT(not smallBuffer.isInlineBuffer());
SC_TEST_EXPECT(smallBuffer.capacity() == 2048);
// Shrinking it will restore its original inline buffer
SC_TEST_EXPECT(smallBuffer.shrink_to_fit());
// And verify that that's actually true
SC_TEST_EXPECT(smallBuffer.isInlineBuffer());
SC_TEST_EXPECT(smallBuffer.capacity() == 128);
}
constexpr T && move(T &value)
Converts an lvalue to an rvalue reference.
Definition: Compiler.h:269
#define SC_TEST_EXPECT(e)
Records a test expectation (eventually aborting or breaking o n failed test)
Definition: Testing.h:113

Function

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

Template Parameters
FuncTypeType of function to be wrapped (Lambda, free function or pointer to member function)
Note
Size of lambdas or less than LAMBDA_SIZE (currently 2 * sizeof(void*)).
If lambda is bigger than LAMBDA_SIZE the constructor will static assert.

Example:

// A regular class with a member function
struct SomeClass
{
float memberValue = 2.0;
int memberFunc(float a) { return static_cast<int>(a + memberValue); }
};
// A Functor with operator ()
struct SomeFunctor
{
float memberValue = 2.0;
int operator()(float a) { return static_cast<int>(a + memberValue); }
};
// Free function
int someFunc(float a) { return static_cast<int>(a * 2); }
// Class too big to be grabbed by copy
struct BigClass
{
SC::uint64_t values[4];
};
void SC::FunctionTest::functionDocumentationSnippet()
{
SomeClass someClass;
Function<int(float)> func;
func = &someFunc; // Bind free func
func.bind<SomeClass, &SomeClass::memberFunc>(someClass); // Bind member func
func = [](float a) -> int { return static_cast<int>(a + 1.5); }; // Bind lambda func
func = SomeFunctor{2.5}; // Bind a functor
// If you feel brave enough you can retrieve the bound functor by knowing its type
SC_ASSERT_RELEASE(func.dynamicCastTo<SomeFunctor>()->memberValue == 2.5f);
// This will static_assert because sizeof(BigClass) is bigger than LAMBDA_SIZE
// BigClass bigClass;
// func = [bigClass](float a) -> int { return static_cast<int>(a);};
}
#define SC_ASSERT_RELEASE(e)
Assert expression e to be true.
Definition: Assert.h:66
unsigned long long uint64_t
Platform independent (8) bytes unsigned int.
Definition: PrimitiveTypes.h:42

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

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 Linux = sizeof(void*);
static constexpr int Default = Linux;
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
}

Blog

Some relevant blog posts are:

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.