This template library is a collection of template-oriented code that we, the Data Science Group at UPB, found pretty handy. It contains:
switch_cases: Use runtime values in compile-time context.integral_template_tuple: Create a tuple-like structure that instantiates a template for a range of values.integral_template_variant: A wrapper type forstd::variantguarantees to only contain variants of the formT<ix>where ix in [first, last) (inclusive, exclusive).integral_sequence: Utilities for generating compile-time integer sequences with automatic direction detection.for_{types,values,range}: Compile time for loops for types, values or rangespolymorphic_allocator: Likestd::pmr::polymorphic_allocatorbut with static dispatchlimit_allocator: Allocator wrapper that limits the amount of memory that is allowed to be allocatedDICE_MEMFN: Macro to pass member functions like free functions as argument.pool&pool_allocator: Arena/pool allocator optimized for a limited number of known allocation sizes.DICE_DEFER/DICE_DEFER_TO_SUCCES/DICE_DEFER_TO_FAIL: On-the-fly RAII for types that do not support it natively (similar to go'sdeferkeyword)overloadedandmatch: Batteries forstd::variant(and alsodtl::variant2). Compose re-usable visitors withoverloador apply a single-use visitor directly withmatch.flex_array: A combination ofstd::array,std::spanand avectorwith small buffer optimizationtuple_algorithms: Some algorithms for iterating tuplesfmt_join: A helper to join elements of a range with a separator for use withstd::formatalike fmt::joinchannel: A single producer, single consumer queuevariant2: Likestd::variantbut optimized for exactly two typesmutex/shared_mutex: Rust inspired mutex interfaces that hold their data instead of living next to itstatic_string: A string type that is smaller thanstd::stringfor use cases where you do not need to resize the stringranges: Additional range algorithms and adaptors that are missing from the standard library.next_to_range/next_to_view/next_to_iter: Eliminate the boilerplate required to write C++ iterators and ranges.inplace_polymorphic:std::variant-like on-stack polymorphism based onvirtualfunctions.type_list: A variadic lists of types for metaprogramming.lazy_conditional: Lazy conditional type selection that only instantiates the selected branch.format_to_ostream: Provide an ostreamoperator<<overload for any type that is formattable withstd::format.stdint: User defined literals for fixed size integers.functional: Extensions for<functional>. Currently, contains abind_frontimplementation with constexpr function argument.DICE_DBG: Prints and returns the value of a given expression for quick and dirty debugging.opt_min/opt_max/opt_minmax: Optional-aware min/max/minmax algorithms that treatstd::nulloptas "no value".pointer_tag_pair: A pointer and value pair that stores a small integer tag in the pointer alignment bits based on the API from P3125 (constexpr pointer tagging).
Use runtime values in a compile-time context. This is realised by instantiating ranges of values at compile-time and
dispatching to the correct version at runtime. You can add fallbacks for when the runtime value lies outside the range
defined. By using switch_cases inside of switch_cases multidimensional ranges can be handled as well. Examples can
be found here.
Create a tuple-like structure that instantiates a template for a range of values. Let's say you have a type like
template <std::size_t N> struct my_type{...};Then you can create a tuple consisting of my_type<i>, my_type<i+1>, ... up to my_type<j - 1> for i < j with this code.
Negative indices, recasting to fewer values and non-default construction are also possible.
Automatic direction detection is used based on the relationship between first and last:
integral_template_tuple<i, j, my_type>creates:my_type<i>, my_type<i+1>, ..., my_type<j-1>wheni < j(ascending, exclusive upper bound[i, j))my_type<i>, my_type<i-1>, ..., my_type<j+1>wheni > j(descending, exclusive lower bound(j, i])- Empty tuple when
i == jExamples can be found here.
Creates a variant-like structure that instantiates a template for a range of values. Let's say you have a type like
template <std::size_t N> struct my_type{...};Then you can create a variant consisting of my_type<i>, my_type<i+1>, ..., my_type<j - 1> with the help of
integral_template_variant<i, j, my_type>.
Negative indices and both ascending and descending ranges are supported.
Automatic direction detection is used based on the relationship between first and last:
integral_template_variant<i, j, my_type>creates a variant of:my_type<i>, my_type<i+1>, ..., my_type<j-1>wheni < j(ascending, exclusive upper bound[i, j))my_type<i>, my_type<i-1>, ..., my_type<j+1>wheni > j(descending, exclusive lower bound(j, i])- Empty variant when
i == j
Examples can be found here.
Provides utilities for working with compile-time integer sequences. This is the foundation for
integral_template_tuple and integral_template_variant.
make_integer_sequence<Int, first, last>: Generatestd::integer_sequencewith automatic direction detectionmake_index_sequence<first, last>: Convenience wrapper forstd::integer_sequence<size_t, X, Y>make_integral_constant_list<Int, first, last>: Generatetype_listofstd::integral_constant<Int, ix>
All utilities automatically determine direction:
first == last: empty sequencefirst < last: ascending[first, last)(exclusive upper bound)first > last: descending(last, first](exclusive lower bound)
Examples can be found here.
Different flavors of compile time loops that allow to iterate types, values or ranges at compile time. Types and values are provided as template arguments and a lambda to be called for each of them is passed as function argument, e.g. for_types<uint8_t, uint64_t>([]<typename T>() {}) and for_values<1, 1.1, 'c'>([](auto x) {}). Ranges are defined by template parameters for start and exclusive end and receive a function to be applied to each range element as function argument, e.g. for_range<3, 5>([](auto x) {}), including support for decreasing ranges and negative indices, e.g. for_range<2, -4>([](auto x) {}). Examples can
be found here.
A std::pmr::polymorphic_allocator-like type that uses static dispatch instead of dynamic dispatch to choose the allocator.
This allocator is primarily useful for situations where you have inhomogeneous memory, and one of the memory
types does not allow dynamic dispatch using vtables; but you still want to mix and match values from both memory types.
For example, you might have some allocations in persistent or shared memory (or generally: memory-mapped allocations) and others on the heap.
The problem with mmap allocations is that they will be placed at an arbitrary position in virtual memory each time they are loaded,
therefore, absolute pointers will cause segfaults if the segment is reloaded.
Which means: vtables will not work (because they use absolute pointers) and therefore you cannot use std::pmr::polymorphic_allocator.
Allocator wrapper that limits the amount of memory that can be allocated through the inner allocator.
If the limit is exceeded it will throw std::bad_alloc.
DICE_MEMFN is a convenience macro that makes it easy to pass member functions as argument, e.g., to range adaptors. It eliminates boilerplate code by creating a lambda that captures this and perfectly forwards arguments to your member function.
A memory arena/pool allocator with configurable allocation sizes. This is implemented
as a collection of pools with varying allocation sizes. Allocations that do not
fit into any of its pools are directly served via new.
A mechanism similar to go's defer keyword, which can be used to defer some action to scope exit.
The primary use-case for this is on-the-fly RAII-like resource management for types that do not support RAII (for example, C types).
Usage examples can be found here.
Some algorithms for iterating tuples, for example tuple_fold a fold/reduce implementation for tuples.
Works just like fmt::join but for std::format.
A combination of std::array, std::span and a vector with small buffer optimization where the size is either
statically known or a runtime variable depending on the extent/max_extent template parameters
A single-producer, single-consume queue. This can be used to communicate between threads in a more high level fashion than a mutex+container would allow.
Like std::variant but specifically optimized for usage with two types/variants.
The internal representation is a union of the two types plus a 1 byte (3 state) discriminant.
Additionally, visit does not involve any virtual function calls.
Things that are missing around std::variant and visitors in the standard library. Implementation of the common overload pattern to create re-usable visitors. Also, comes with a match function that allows you to declare the visitor directly inline when applying it.
Things that are missing in the standard library <type_traits> header.
Rust inspired mutex interfaces that hold their data instead of living next to it. The benefit of this approach is that it makes it harder (impossible in rust) to access the data without holding the mutex.
A string type that is smaller than std::string but does not have the ability to grow or shrink.
This is useful if you never need to resize the string and want to keep the memory footprint low.
It also supports allocators with "fancy" pointers.
Additional range algorithms (e.g. unique_view) and adaptors (e.g., a pipeable all_of)
that are missing from the standard library.
Eliminate the boilerplate required to write C++ iterators and ranges. To get a fully functional range, the only thing that is required is implementing a minimal, rust-style iterator interface.
Due to technical reasons in how std::ranges::ranges are specified, next_to_view is not suitable
to build truly general purpose "transformer"/pipeline views (i.e. ranges that take existing ranges and do stuff to them).
It is possible to build "transformer" views, but you need to limit yourself to copyable views inside of them, i.e. you cannot use
std::ranges::owning_view (which is only movable), you must use std::ranges::ref_view. Therefore, it would not be possible to
move a vector inside a pipeline that has a next_to_view-based view as part of it.
(It works fine if you don't move the vector into the pipeline.)
Similar to std::variant, this type allows for polymorphism without heap allocations.
Unlike std::variant, it uses polymorphism based on virtual functions instead of std::visit/std::get.
It is meant as variant-like stack-storage for similar types (i.e. types in an inheritance hierarchy).
This greatly reduces the boilerplate compared to std::visit based on-stack polymorphism.
A variadic list of types for use in metaprogramming. Optimized to be much faster than the equivalent code
written with std::tuple. It supports various operations similar to what you would use for
std::ranges (e.g. transform, filter, etc.).
A lazy alternative to std::conditional that only instantiates the selected branch.
Unlike std::conditional, which eagerly evaluates both branches, lazy_conditional only accesses
the ::type member of the branch that is selected. This prevents compilation errors when
the non-selected branch would be ill-formed (e.g., contains a static_assert(false) or
accesses invalid type members).
Additionally, lazy_switch provides multi-way conditional type selection using a first-match
approach with case_<bool, Provider> helpers. Only the selected case's provider is instantiated,
allowing you to use static_assert(false) in default cases that should never be reached.
Examples can be found here.
Provide an ostream operator<< overload for any type that is formattable with std::format.
Does not override preexisting operator<< implementations.
The primary usage for this is for doctest tests, because doctest only supports output via std::ostream (not std::format).
Note: for this to work it needs to be included before <doctest/doctest.h>.
User defined literals for fixed size integers (e.g. 123_u64).
This is mainly useful for cross-platform applications where the common 123ul is not always the same as uint64_t{123}.
For instance, on macOS uint64_t is defined as unsigned long long, whereas on Linux it is defined as unsigned long.
Even if both unsigned long and unsigned long long have the same size, they are still distinct types which can cause issues
when a type is being deduced.
Extensions for <functional>.
Currently, contains an implementation of bind_front with constexpr function argument (bind_front<constexpr_func>(args...))
that is only available from C++26 onwards.
A macro for debugging inspired by rust's dbg! macro.
It prints and returns the value of a given expression.
Optional-aware min/max/minmax algorithms. They treat std::nullopt as "no value".
Like in the standard-library, they are available as 2-argument/initializer-list functions (opt_min, opt_max, opt_minmax),
and range-based variants (opt_min_element, opt_max_element, opt_minmax_element).
Returns std::nullopt when all inputs are empty.
The result type is deduced automatically from the arguments. To specify it explicitly, pass the type
as a template argument, e.g. opt_min<double>({5, 3, 8}).
All functions accept an optional custom comparator as the last argument,
e.g. opt_min<int>({5, 3, 8}, std::greater{}) or opt_min_element(v, std::greater{}).
The comparator replaces operator< and should return true if the first argument is less than the second.
Examples can be found here.
Compilable code examples can be found in examples. The example build requires the cmake
option -DBUILD_EXAMPLES=ON to be added.
A C++23 compatible compiler. Code was only tested on x86_64.
You can use it with conan.
To do so, you need to add dice-template-library/2.6.0 to the [requires] section of your conan file.
# get it
git clone https://github.com/dice-group/dice-template-library.git
cd dice-template-library
wget https://raw.githubusercontent.com/conan-io/cmake-conan/refs/heads/develop2/conan_provider.cmake
# build it
mkdir build
cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DBUILD_EXAMPLES=ON -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=conan_provider.cmake -B build .
cmake --build build --parallel $(nproc)
# run tests
cd build
ctest
# run an example
cd build
./examples/examples_dbg