This is a collection of tools/utilities I wrote over time. Most parts are independent of eachother, some rely on eachother.
It is sort of a general utilities library, but you are unlikely go use every part of it.
"Werkzeug" means "tool" in german, hence the name.
Primarily me and anybody who is interested. I wrote these things out of curiosity/because they seemed interesting.
Maybe you can take something from it.
While its technically a lot of seperate parts, there are common utilities that are used across many different files.
The library currently provides a simple CMake script allowing its in-tree usage, but no installation utilities.
- Expected: A slightly improved alternative to
std::expected - inheritance_variant: A variant-like sum type
- overload: A tool to easily create overloads from multiple callables
- aggregate_ordering: Utility to ad-hoc create a complex ordering relation, e.g. for use in sorting algorithms
- type_pack, value_pack: Wrappers to enable easy handling of parameter packs
- type_traits: A large wrapper for many type traits
- Callable_Wrapper: A wrapper to allow passing C++ callables to C-APIs
- explicit_: A wrapper that disallows implicit conversions
- select_overload': Utility to select a specific overload form an overload set
- try_constexpr_invoke: Utility to potentially evaluate a function at compile time
- State_Machine: Utility to create simple state machines
- Nd array types: Nd linearly indexed arrays/tables (similar to C++23
std::mdarray). Multiple versions with different axis settings, as well as interpolating versions exist.
The container/ and memory/ parts are mainly rewrites of existing (standard library) tools for exploration/technical learning purposes. The things in simple_vector_implementation/ are iterative, simpler versions of vector. They mostly exist for learners to look at with less "feature related clutter". A complete and advanced vector implementation is in container/dynamic_array.hpp.
Note that not everything in here is of similar implementation quality/completeness. Some things are as complete as can be with correct noexcept specifiers, others may lack all basic functionality.
This is an overview of all the seperate files and their contents:
For additional examples you can look at the tests/ (if they exist...)
This exists to easiy create wrappers allowing using a generic C++ callable with a C language API that accepts a void* for any user data.
This is mostly useful when wanting to use established C libraries in C++ code. A prime example would be GNU scientific libraries.
// some C API.In this example it just invokes our function
double c_api_mid( double a, double b, double(*f)(double a, void* ud, double b), void* ud )
{
return f(a,ud,b);
}
// Your C++ callable. Capture only to make it non-trivial to convert to a function pointer.
constexpr auto callable = [&]( double a, double b )
{
return a+b;
};
// create the callable wrapper
auto wrapper = werkzeug::make_wrapper<double(double,double,user_data)>( callable );
// calling the C API gives us the exact same value as calling
REQUIRE( callable(0,1) == c_api_tail( 0, 1, wrapper, &wrapper ) );This is an extended version of std::expected. Notably it allows you to directly specifc multiple error types (instead of having to manually interact with a variant).
Its monadic interface automatically transforms values and transfers errors along.
enum class err_a { a1 }; // some error codes for demo purposes. Could also be any other type.
enum class err_b { b1 };
Expected<int,err_a> expected_a = 0; // expected now contains an int
Expected<int,err_a> expected_b = err_a::a1; // expected now contains an err_a
{ // using monadic interface transforming the result
constexpr auto transform = []( int i ) -> long
{
return i;
}
Expected<long,err_a> res_a = expected_a.and_then( transform ); // res_a now contains the long 0
Expected<long,err_a> res_b = expected_b.and_then( transform ); // res_a now contains the err_a
}
{
constexpr auto transform_to_expected( int i ) -> Expected<long,err_b>
{
if ( i == 0 )
{
return i;
}
else
{
return err_b::b1;
}
}
Expected<long,err_a,err_b> res_a = expected_a.and_then( transform_to_expected ); // res_a now contains the long 0
expected_a.emplace( 1 ); // changes the value in `expected_a` to 1
Expected<long,err_a,err_b> res_a2 = expected_a.and_then( transform_to_expected ); // res_a2 now contains err_b::b1
Expected<long,err_a,err_b> res_b = expected_b.and_then( transform_to_expected ); // res_b now contains err_a::a1
}A helper type that does not allow implicit conversions
This is a variant-like sum type that only allows storage of a compile time known list of types derived from a common Base. It provieds access to a base pointer, allowing for slightly cleaner usage without having to go through std::visit.
It does not require a virtual destructor. The objects are stored within the variant and destroyed according to their index.
The provided API is largely identical to that of std::variant
enum class type
{
base, derived
};
struct Base
{
virtual type type_id() const
{
return type::base;
}
// no virtual destructor required for the usage
};
struct Derived : Base
{
virtual type type_id() const override
{
return type::derived;
}
};
using variant_t = werkzeug::inheritance_variant<Base, Derived>;
variant_t var{ Base{} };
REQUIRE( var->type_id() == type::base );
var.emplace<Derived>();
REQUIRE( var->type_id() == type::derived );This is an extension upon the classic pattern
template<typename...Ts>
struct overload : Ts...
{
using Ts::operator() ...;
};Notably this overload also takes both member function and member data pointers, allowing a usage similar to std::invoke, but with less overhead:
struct A
{
int f();
};
struct B
{
int i = 0;
};
constexpr overload my_overload = {
&A::f,
&B::i,
}
int a = my_overload(A{}); // invokes A::f
int b = my_overload(B{}); // accesses and returns B::iIf the provided projectors do not have a common return type, the return type of overload::operator() will be void and potential results will be discarded.
This is a helper to select a selecfic signature from an overload set.
int f( int );
void f( double );
constexpr auto f_int = select_overload<int>( &f );This header contains utilities for dealing with parameter packs. For this it primarily uses type_pack<Ts...> and value_pack<auto...>.
Both provide the same functionalities:
List of provided utilities:
type_pack_c<T>: concept to check whetherTis an instantiation oftype_packvalue_pack_c<T>: concept to check whetherTis an instantiation ofvalue_packpack::size: static member containing the size of the packpack::contains<X>(): static member returning whetherpackcontainsX(type or value)pack::index_of<X>(): static member returning the index ofXinpacktype_pack::type_at<I>: static member typedef, giving the type at indexIvalue_pack::value_at<I>: static data member, giving the value at indexItype_at_index<I,type_pack>: freestanding metafunction get thet type atI. Can also be used astype_at_index<I,Ts...>.value_at_index<I,valze_pack>: freestanding metafunction to get the value at indexI. Can also be used asvalue_at_index<I,Vs...>all_unique<Ts...>,all_unique<type_pack>,all_unique<value_pack>: freestanding metafunction to querry if all elements in a pack are unique.pack_inclues<pack1,pack2>: freestanding metafunction to check whetherpack1fully includespack2append<pack1,X>: freestanding metafunction that appendsXtopack1. Can also be used asappend<pack1,pack2>to append the entire packappend_if_unique<pack1,X>: freestanding metafunction that appendsXtopack1, ifpack1does not already containXstatic_for<pack>( Callable&& callable ): function that invokesCallablefor every type or value inpack, passing it as a distinct type, so it can be used as a constant expression
This provides a utility to create a rudimentary state machine from an enum and functions.
// an enum for our states
enum class states : unsigned char
{
A, B
};
// the machine itself
struct Flip_Flop_Machine
: werkzeug::State_Machine<states,2,Flip_Flop_Machine>
{
// some custom members
int i = 0;
using Base = werkzeug::State_Machine<states,2,Flip_Flop_Machine>;
// just a default ctor
constexpr Flip_Flop_Machine()
: Base{ states::A }
{ };
// declare the state function template.
template<states>
states func_for() = delete; //delete its default. That means that the compiler catches if we have not implemented all of them
};
// implement the state function for states::A
template<>
states Flip_Flop_Machine::func_for<states::A>()
{
std::cout << "FLIP\n";
++i;
return states::B; // returns the next state we want to go to.
}
// implement the state function for states::B
template<>
states Flip_Flop_Machine::func_for<states::B>()
{
std::cout << "flop\n";
++i;
return states::A; // returns the next state we want to go to.
}
int main()
{
Flip_Flop_Machine m;
m.execute_active() // prints "FLIP"
m.execute_active() // prints "flop"
REQUIRE( m.i == 2 );
}This header provides a generic type trait type_traits<T>, that contains almost all information about a type.
For every operation, there is named member type trait, that tells you if the operation is possible, trivial or nothrow.
traits<T>::default_construct::possibletraits<T>::swap::nothrowtraits<T>::copy_construct::trivialtraits<T>::template invocable<Args...>::possibletraits<T>::subscript::nothrow
All member traits follow the pattern of providing possible, trivial and nothrow to check the respective property.
Pretty much std::integral_constant with a better name.
Provides a user defined literal _tc that automatically creates a size_t or long double type_constant:
constexpr auto A = 0_tc; // type_constant<0,size_t>{}
constexpr auto B = 0.0_tc; // type_constant<0.0l,long double>{}An exploration into creating a type erased wrapper that provides specifications for an interface.
struct S
{
int i;
};
std::ostream& operator<<( std::ostream& os, const S& s )
{
return os << s.i;
}
std::stream& operator>>( std::istream& is, const S& s )
{
return is >> s.i;
}
Shared_Object obj{ S, Generic_Stream_Extraction<S>{}, Generic_Stream_Insertion<S>{} }; // create an erased object with its set of supported operations
std::string_stream ss("42");
obj.operation<Stream_Extraction>()(ss); // `Stream_Extraction` is a predefined interface that is implemented by the `Generic_Stream_Extraction<T>`
obj.operation<Stream_Insertion>()(std::cout); // prints 42
if ( auto op_ptr = obj.try_operation<Unsupported_Operation>() )
{
// never happens, because no `Unsupported_Operation` was provided.
}
if ( auto op_ptr = obj.try_operation<Stream_Insertion>() )
{
(*ptr)( std::cout ); // prints 42 again
}This utility will try to invoke a callable at compile time. If that is possible, it provides the value. Otherwise it does not.
constexpr int yes()
{ return 0; }
constexpr auto result = try_constexpr_invoke( yes );
static_assert( result.has_value() );
static_assert( result.value() == 0 );int no()
{ return 0; }
constexpr auto result = try_constexpr_invoke( no );
static_assert( not result.has_value() );This is a fancy little utility that easily lets you create custom ordering ad-hoc
// some data structure
struct S
{
int i;
int j;
double d;
};
constexpr auto ordering = aggregate_ordering{ // sort by
by{ &S::i, less{} }, // ascending i first
by{ &S::d, less{} }, // ascending d next
by{ &S::j, greater{} } // descending j last
};
S arr[] = {
{ .i = 0, .j= 1, .d=2.0 },
{ .i = 2, .j= 2, .d=3.0 },
{ .i = 2, .j= 2, .d=2.0 },
{ .i = 0, .j= 3, .d=2.0 },
{ .i = 3, .j= 4, .d=2.0 }
};
std::ranges::stable_sort( arr, ordering );
REQUIRE( arr[0].i == 3 );
REQUIRE( arr[1].i == 2 );
REQUIRE( arr[1].d == 3.0 );
REQUIRE( arr[2].i == 2 );
REQUIRE( arr[2].d == 2.0 );
REQUIRE( arr[3].j == 1 );These array types are similar to std::mdarray, but are C++17 compatible. Their primary purpose is the implementation of the Interpolating_ table types.
Array_NDis simply an ND array, supporting indexing viaoperator()Bounded_Array_Ndalso has lower and upper bounds attachted to each axisAxis_Tick_Array_Ndinstead has ticks for every dimension saved, meaning they can be non-linearly/logarythmically spaced.Basic_Interpolating_Array_Ndis aBounded_Array_Ndthat can interpolate at an arbitrary position in the hypercube, or do simple extrapolation outside of it.Advanced_Interpolating_Array_Ndis anAxist_Tick_Array_Ndthat supports interpolation.
Notable features:
- All of them also provide a member function
make_view()that creats astd::spanlike object to view the array/table. These objects support the exact same functionalities as the owning array itself. - These types can be saved to an
std::ostreamand constructed from anstd::ostream - Interpolation works in N dimensions, either as lin, log or linlog interpolation.
The saving and loading works in a plain text format, layed out as such:
Array_Nd:
[ axis lengths ] [ data ]
Bounded_Array_Nd
[ Array_Nd ] [ axis_lower_bounds ] [ axis_upper_bounds ] [ axis_is_log ]
Basic_Interpolating_Table
[ Bounded_Array_Nd ]
Axis_Tick_Array_Nd
[ Array_Nd ] [ axis_ticks ]
[ Advanced_Interpolating_Table ]
[ Axis_Tick_Array_Nd ]
- The tests use doctest under the MIT license