vari
Loading...
Searching...
No Matches
vari

Variadic library

Example - Documentation

C++ has std::variant<Ts...> as a tagged union, but we find it lacking in capabilities and just plain ... bad.

vari introduces enhanced alternatives with a more advanced API, flexible type conversions, and improved ways to define variadic types.

The library introduces four basic types:

  • vptr<Ts...> - A pointer to any type out of Ts..., which can be null.
  • vref<Ts...> - A reference to any type out of Ts....
  • uvptr<Ts...> - A unique (owning) pointer to any type out of Ts..., which can be null.
  • uvref<Ts...> - A unique (owning) reference to any type out of Ts....

Do you have any questions? Feel free to contact veverak on #include discord.


  • vref and vptr
  • uvptr and uvref
  • Access API
    • Visit
    • Take
  • Sub-typing
  • Concepts checks
  • Single-type extension
  • Type-sets
  • Lvalue conversion from unique
  • Const
  • Deleter
  • Typelist compatibility
  • vcast
  • Dispatch
  • Template deduction
  • Credits

vref and vptr

vref and vptr are used to point to any type from a specified list of types. The vref always points to a valid object, whereas vptr can be null.

auto foo = [&](vref<int, float> v){
v.visit([&](int& i) { std::cout << "this is int: " << i << std::endl;},
[&](float& f){ std::cout << "this is float: " << f << std::endl;});
};
int i;
foo(i); // << vref<int,float> points to `i`
float f;
foo(f); // << vref<int, float> points to `f`

Here foo accepts a vref that references either an int or a float.

uvptr and uvref

The u-prefixed variants (uvptr and uvref) imply unique ownership, which means they manage the lifetimes of objects.

struct a_t{};
struct b_t{};
_define_variadic< _uvptr, typelist< Ts... >, def_del > uvptr
A nullable owning pointer to types derived out of Ts... list by flattening it and filtering for uniqu...
Definition: uvptr.h:338

Similar to std::make_unique, we provide a function uwrap for construction of unique variants:

uvref<std::string, int> p = uwrap(std::string{"wololo"});

Here uwrap creates uvref<std::string> which gets converted into uvref<std::string,int> due to implicit conversion.

WARNING: uvref is movable, and when moved from, it enters a null state. It shall not be used in this state except for reassignment.

Access API

To access the underlying type, vptr, vref, uvptr, and uvref use the visit method as the primary interface. The u variants also have take to transfer ownership.

Visit

The visit method works similarly to std::visit but allows multiple callables:

auto foo = [&]( vari::vref< std::vector< std::string >, std::list< std::string > > r ) -> std::string&
{
std::string& front = r.visit(
[&]( std::vector< std::string >& v ) -> std::string& {
return v.front();
},
[&]( std::list< std::string >& l ) -> std::string& {
return l.front();
} );
return front;
};
_define_variadic< _vref, typelist< Ts... > > vref
A non-nullable pointer to types derived out of Ts... list by flattening it and filtering for unique t...
Definition: vref.h:162

For pointers, there must be a callable that accepting vari::empty_t to handle cases where the pointer is null:

r.visit([&](vari::empty_t){},
[&](int&){},
[&](std::string&){});
_define_variadic< _vptr, typelist< Ts... > > vptr
A nullable pointer to types derived out of Ts... list by flattening it and filtering for unique types...
Definition: vptr.h:189

Variadic references can be constructed with references to any of the possible types:

std::string a;

This also allows us to combine it with visit, where the callable can handle multiple types:

r.visit([&](vari::empty_t){},

Or we can mix both approaches:

r.visit([&](vari::empty_t){},
[&](std::string&){},

Take

uvref and uvptr retain ownership of referenced items, the take method is used to transfer ownership:

{
std::move(r).take([&](vari::uvref<int>){},
};
_define_variadic< _uvref, typelist< Ts... >, def_del > uvref
A non-nullable owning pointer to types derived out of Ts... list by flattening it and filtering for u...
Definition: uvref.h:281

Sub-typing

All variadic types support sub-typing, meaning any variadic type can be converted into a type that represents a superset of its types:

std::string a;
// allowed as {int, std::string} is superset of {std::string}
// not allowed, as {int} is not superset of {int, std::string}
vari::vref<int> p3 = p2; // error: not allowed

This feature also works seamlessly with take:

struct a_t{};
struct b_t{};
struct c_t{};
struct d_t{};
{
std::move(p).take([&](vari::empty_t){},
};

In this example, p represents a set of four types. The take method allows us to split this set into four unique references, each representing one type. Sub-typing enables us to combine these references into two subsets, each consisting of two types.

Note: As a side-effect of this, vptr<a_t, b_t> is naturally convertible to vptr<b_t, a_t>

Concepts checks

Access methods are subject to sanity checks on the set of provided callbacks: for each type in the set, exactly one callback must be callable.

For instance, the following code would fail to compile due to a concept check violation:

{
r.visit([&](int&){},
[&](int&){}, // error: second overload matching int
[&](std::string&){});
};

Note that this rule also applies to templated arguments:

{
r.visit([&](int&){},
[&](auto&){}, // error: second overload matching int
[&](std::string&){});
};

Another important check: each callable must be compatible with at least one type in the set.

int i = 42;
v.visit([&](int&){},
[&](std::string&){}, // error: callable does not match any type
[&](float&){});

Single-type extension

To make working with variants more convenient, all variadic types allow direct access to the pointed-to type if there is only a single type in the type list:

struct boo_t{
int val;
};
boo_t b;
p->val = 42;

This feature makes vref a useful replacement for raw references in structures:

struct my_type{
vref<std::string> str;
};

If you used std::string& str, it would prevent the ability to reassign the reference within the structure. vref, however, does not have this limitation, allowing reassignment.

Type-sets

For added convenience and functionality, the template argument list of variadic types can flatten and filter types for uniqueness.

Given the following type sets:

using set_a = vari::typelist<int, std::string>;
using set_b = vari::typelist<float, int>;
using set_s = vari::typelist<set_a, set_b, std::string>;

The pointer vptr<set_s> resolves to the equivalent of vptr<int, std::string, float>. The flattening and filtering mechanism only applies to vari::typelist. For example, void<std::tuple<a_t,b_t>> would not be automatically resolved to a different form.

Why is this useful? It allows expressing complex data structures more effectively. Moreover, the typelists also interact well with sub-typing:

using simple_types = vari::typelist<std::string, int, bool>;
struct array_t{};
struct object_t{};
using complex_types = vari::typelist<array_t, object_t>;
using json_types = vari::typelist<simple_types, complex_types>;
auto simple_to_str = [&](vari::vref<simple_types> p) { return std::string{}; };
auto to_str = [&](vari::vptr<json_types> p)
{
using R = std::string;
return p.visit([&](vari::empty_t) -> R { return ""; },
[&](vari::vref<simple_types> pp) -> R { return simple_to_str(pp); },
[&](array_t& pp) -> R { /* impl */ },
[&](object_t& pp) -> R { /* impl */ });
};

This approach makes it easier to handle complex type hierarchies while preserving the flexibility and power of variadic types.

Pretty printer

./pprinter.py is pretty printer for vari for gdb. Just use source path/to/vari/pprinter.py.

Lvalue conversion from unique

For added convenience, the library allows converting uvptr and uvref to their non-unique counterparts, but only if the expression is an lvalue reference:

auto foo = [&](vari::vptr<int, std::string>){};
foo(p); // allowed, `p` is lvalue
foo(vari::uvptr<std::string>{}); // error: rvalue conversion forbidden

Const

All variadic types support conversion from non-const version to const version:

const is also properly propagated during typelist operations:

using set_a = vari::typelist<int, std::string>;

Both types vp_a and vp_b are compatible.

Deleter

uvref and uvptr delete objects when appropiate. This can be customized by specifying the deleter type.

This is not possible by the uvref and uvptr type aliases directly, but by directly using the underlying _uvref and _uvptr classes, where Deleter is the first template argument. (WARNING: _-prefixed symbols can be subject to backwards-incompatible changes in future development)

The Deleter has to be a callable object. It can be called with a pointer to any of the types referenced to by the variadics. The call signals release of the object by the variadics. Default implementation vari::def_del calls delete on the pointers.

The API for specifying custom Deleter to variadics mirrors the API of std::unique_ptr. Construction and assignment of variadics should behave the same way as std::unique_ptr.

Typelist compatibility

Library can be extended by using other types than just vari::typelist to represent set of types.

Whenever type T is typelist is determined by vari::typelist_traits<T>. In case vari::typelist_traits<T>:::is_compatible evaluates to true, library considers T to be typelist-like type.

In such a case, vari::typelist_traits<T>::types should be a type which by itself is vari-compatible typelist. Transtively, this should eventually resolve into vari::typelist itself which is used by the library directly.

vcast

vcast<T>(r) static casts any item of reference r to T. Handy utility to access common base of multiple types:

struct base{};
struct a : base{};
struct b : base{};
a a1;
auto& b = vari::vcast<base&>(p);

Dispatch

We also ship free function dispatch for mapping a runtime value into compile-time value. It uses all the safety checks of visit:

// factory used by the library to create instances of types
auto factory = [&]<vari::index_type i>{
return std::integral_constant<std::size_t, i>{};
};
// runtime index
vari::index_type v = 2;
vari::dispatch<3>(
v,
factory,
[&](std::integral_constant<std::size_t, 0>){
},
[&](std::integral_constant<std::size_t, 1>){
},
[&](std::integral_constant<std::size_t, 2>){
});

Template deduction

Usage of template deduction won't work directly with vari types. vref, vptr, uvref, uvptr are aliases of their _ prefixed implementation. This alias is what does the typelist processing and filtering. As a consequence, any attempt at using C++ template argument deduction won't work:

auto foo = [&]<typename ... Ts>(vari::vptr<Ts...> vr){};
foo(x); // error: won't deduce the parameters as `vptr` is alias
foo.operator()<int, std::string>(x); // works because types are explicit

If this mechanics is deemed necessary, developers has to match against the underlying template, for which we do not guarantee that it won't change between udpates to library. No guarantees here.

auto foo = [&]<typename ... Ts>(vari::_vptr<Ts...> vr){};
foo(x); // works
A nullable pointer to one of the types in Ts...
Definition: vptr.h:42

Credits

Credits for the idea for this should go to avakar, live long and prosper.