The any module provides the seq::hold_any
class and related functions to provide a type-erasing polymorphic object wrapper.
seq::hold_any
is a std::any
like class optimized to create heterogeneous containers of any types (vectors, deques, hash tables, sorted containers...).
The seq library provides the following aliases:
seq::any
: equivalent toseq::hold_any<seq::any_default_interface>
seq::r_any
: relocatable version ofseq::any
seq::nh_any
: equivalent toseq::hold_any<seq::any_no_hash_interface>
seq::r_nh_any
: relocatable version ofseq::nh_any
hold_any
offers a similar interface to std::any
and supports additional features:
- Support for comparison operators <, <=, >, >=, == and !=
- Support for standard stream operators to/from std::ostream/std::istream
hold_any
is hashable based on std::hashhold_any
can be formatted using the format module of seq libraryhold_any
interface can be extended
hold_any
is very similar to boost.TypeErasure or folly.Poly, but its default implementation provides more features in order to be used within most containers (sorted containers, hash tables,...).
These features do not modify the requirements of held types: they must be at least move only contructible.
hold_any
uses type erasure to provide custom behavior based on held object type, and the additional functions like streaming or comparison support are implemented if the type supports them.
If not, the corresponding functions will throw a std::bad_function_call exception:
std::cout << seq::any(2); // works as expected, print '2'
std::cout << seq::any(std::vector<bool>()); // std::vector<bool> does not define stream operator to std::ostream, throw a std::bad_function_call
The only exception is the hashing support. Indeed, there is no way in C++11 to tell at compile time if a type is hashable or not, as most implementations of std::hash
use static_assert() in the operator() member (undetectable through SFINAE).
hold_any
optimistically assumes that all types are hashable. In order to store non hashable types, you must specialize the seq::is_hashable
type trait for this type:
struct test{};
template<>
struct seq::is_hashable<test> : std::false_type {};
//...
seq::any a = test(); // compile as is_hashable is specialized for test class
Otherwise, you can completely disable the hashing support using seq::nh_any
(equivalent to seq::hold_any<seq::any_no_hash_interface>
) :
struct test{};
seq::nh_any a = test();
hold_any
can be casted to another type using either hold_any::cast()
function or seq::any_cast()
(similar to std::any_cast
).
When casting to the same type as the underlying object, it is possible to retrieve a reference to the object:
seq::any a = 2;
int &value1 = a.cast<int&>();
int &value2 = seq::any_cast<int&>(a);
int *value3 = seq::any_cast<int>(&a);
hold_any
supports casting to another type. By default, the following conversion are valid:
- A pointer can be casted to void* or const void*
- All arithmetic types can be casted between each other
- Arithmetic types can be casted to
std::string
orseq::tiny_string
- All string types can be casted between each other (std::string, seq::tstring, seq::tstring_view, std::string_view, char* and const char*)
- All string types can be casted to arithmetic types (which can throw a std::bad_cast exception if the string does not represent an arithmetic type)
Example:
using namespace seq;
any a = "1.2"; // holds a const char*
char* c = a.cast<char*>(); // const char * to char* is valid (although not safe)
void* v = a.cast<void*>(); //conversion to void* is valid (although not safe)
// conversion to the different string types
tstring str = a.cast<tstring>();
tstring_view view = a.cast<tstring_view>();
std::string str2 = a.cast<std::string>();
// conversion to arithmetic
double d = a.cast<double>(); // d holds 1.2
int i = a.cast<int>(); // i holds 1
a = 1.2;
i = a.cast<int>(); //valid, cast double to int
str2 = a.cast<std::string>(); //valid, str2 holds "1.2"
It is possible to register additional conversion functions for custom types using seq::register_any_conversion():
// Dumy pair of int defining an implicit conversion operator to std::string
struct my_int_pair
{
int a, b;
my_int_pair(int a = 0, int b = 0)
:a(a), b(b) {}
// define conversion operator to std::string
operator std::string() const {
std::string res;
seq::fmt(a).append(res);
seq::fmt(b).append(res);
return res;
}
};
// Conversion function from std::pair<int, int> to std::string
std::string pair_to_string(const std::pair<int, int>& p)
{
std::string res;
seq::fmt(p.first).append(res);
seq::fmt(p.second).append(res);
return res;
}
// Register implicit conversion (from my_int_pair to std::string)
seq::register_any_conversion<my_int_pair, std::string>();
// Register explicit conversion function (from std::pair<int, int> to std::string)
seq::register_any_conversion<std::pair<int, int>, std::string>(pair_to_string);
// Disable hashing interface
using my_any = seq::nh_any;
// Create any object containing a std::pair<int, int>
my_any a = std::pair<int, int>(1, 2);
// Cast a to std::string
std::cout << a.cast<std::string>() << std::endl;
// Create any object containing a my_int_pair
my_any b = my_int_pair(1, 2);
// Cast a to std::string
std::cout << b.cast<std::string>() << std::endl;
As said previously, seq::hold_any
is hashable and a specialization for std::hash
is provided.
This feature is mandatory when inserting any objects into hash table based containers like std::unordered_set.
In order to store non hashable types (that do not specialize std::hash), you must specialize seq::is_hashable
type traits for this type to have seq::is_hashable::value == false.
Another way to store non hashable types is to use seq::nh_any instead of seq::any which disable the hashing support.
Attempting to call seq::hold_any::hash()
member on a non hashable type will throw a std::bad_function_call.
The specialization of std::hash for hold_any
is transparent to support heterogeneous lookup.
Example:
#include <iostream>
#include <seq/ordered_set.hpp>
#include <seq/tiny_string.hpp>
#include <seq/any.hpp>
// For transparent equal_to in C++11
#include <seq/utils.hpp>
//...
using namespace seq;
// build an ordered set than supports heterogeneous lookup
seq::ordered_set<any,std::hash<any> , seq::equal_to<void> > set;
set.insert(3); // insert integer
set.insert(2.5); // insert double
set.insert(tstring("hello")); // insert seq::tstring
set.insert(1); // insert integer
set.insert(std::string("world")); // insert std::string
set.insert("ok"); // insert const char*
// print the set content
for (auto val : set)
std::cout << val << std::endl;
assert(set.find(any(3)) != set.end()); // standard lookup
assert(set.find(3) != set.end()); // use heterogeneous lookup
assert(set.find(2.5) != set.end()); // use heterogeneous lookup
assert(set.find("hello") != set.end()); // use heterogeneous lookup
assert(set.find(tstring("world")) != set.end()); // use heterogeneous lookup
assert(set.find(tstring_view("ok")) != set.end()) ; // use heterogeneous lookup
assert(set.find("ok") == set.end()) ; // "ok" not found has we compare 2 const char* -> pointer comparison, not string comparison
assert(set.find("no") == set.end()); // failed lookup
hold_any
objects are equally comparable. This feature is mandatory when inserting any objects into hash table based containers like std::unordered_set
.
Two hold_any
are considered equal if:
- They are both empty.
- They hold the same type and both underlying objects compare equals. If the type does not provide a comparison operator, the operator==() always return false.
- They both hold an arithmetic value of possibly different types, and these values compare equals.
- They both hold a string like object (std::string, seq::tstring, seq::tstring_view, std::string_view, char*, const char*) that compare equals. Note that a const char* can be compared to another string object (like std::string) using string comparison, but comparing two const char* will result in a pointer comparison!
It is possible to register a comparison function for unrelated types using seq::register_any_equal_comparison()
function.
Example:
seq::any a = 1.0;
seq::any b = 1;
seq::any c = "hello";
seq::any d = std::string("hello");
assert(a == b); // compare any(double) and any(int)
assert(a == 1.); // compare any(double) to a double
assert(a == 1); // compare any(double) to int
assert(a != 1.2); // compare any(double) to a double
assert(a != c); // compare any(double) to any(const char*)
assert(c == d); //compare any(const char*) to any(std::string)
assert(d == "hello"); // compare any(std::string) to const char*
// define a comparison function between std::pair<int,int> and int
struct equal_pair
{
bool operator()(const std::pair<int,int> & a, int b) const
{
return a.first == b && a.second == b;
}
};
seq::register_any_equal_comparison<std::pair<int,int>, int>(equal_pair{});
seq::nh_any pair = std::pair<int,int>(2,2);
seq::nh_any integer = 2;
assert(pair == integer);
hold_any
object can be compared using operators <, >, <=, >=, all based on the less than operator. This is mandatory when inserting any objects into a sorted container like std::set.
A hold_any
object A is considered less than another hold_any
B if:
- A is empty and not B.
- A and B hold the same type and object(A) < object(B). If the type does not provide a less than operator, throw std::bad_function_call.
- They both hold an arithmetic value of possibly different types, and object(A) < object(B).
- They both hold a string like object (std::string, seq::tstring, seq::tstring_view, std::string_view, char*, const char*) and object(A) < object(B).
- For totally unrelated types, returns get_type_id(A) < get_type_id(B).
It is possible to register a comparison function for unrelated types using seq::register_any_less_comparison()
function.
Example:
seq::any a = 1;
seq::any b = 2.3;
seq::any c = "hello";
seq::any d = std::string("world");
assert(a < b); // compare any(int) and any(double)
assert(a < 2.3); // compare any(int) to a double
assert(a <= 1); // compare any(int) to int
assert(b >= a); // compare any(double) to any(int)
assert(c < d); //compare any(const char*) to any(std::string)
assert(d > c); // compare any(std::string) to const char*
// define a dumy comparison function between std::pair<int,int> and int
struct less_pair
{
bool operator()(const std::pair<int,int> & a, int b) const
{
return a.first < b && a.second < b;
}
};
seq::register_any_less_comparison<std::pair<int,int>, int>(less_pair{});
seq::nh_any pair = std::pair<int,int>(1,2);
seq::nh_any integer = 3;
assert(pair < integer);
hold_any
provides a customizable Small Buffer Optimization (SBO) in order to avoid costly memory allocation for small types.
The alignment of the small buffer can also be modified.
By default, the small buffer size is equal to sizeof(double) and its alignment is alignof(double). Therefore, on 64 bits machines, sizeof(seq::any) is 16 bytes.
The small buffer size and alignment can be changed through hold_any
template parameters:
// define an any type that can hold a tstring object on the stack
using my_any = seq::hold_any<seq::any_default_interface, sizeof(seq::tstring), alignof(seq::tstring) >;
// the tstring object is stored on the stack using the small buffer. This line does not trigger a memory allocation as tstring itself uses Small String Optimization.
my_any a = seq::tstring("hello");
Internally, hold_any
uses a tagged pointer to store 3 additional information on the held type object:
- Tells if the type is relocatable (
seq::is_relocatable<Type>::value == true
) - Tells if the type is trivially copyable (
std::is_trivially_copyable<Type>::value == true
) - Tells if the type is trivially destructible (
std::is_trivially_destructible<Type>::value == true
)
These information are used to optimize the copy, move assignment and destruction of hold_any
objects.
Note that, by default, hold_any
itself is NOT relocatable as it might hold a non relocatable type inside its small buffer.
It is possible to force hold_any
type to be relocatable by setting the template parameter Relocatable to true.
In this case, small but non relocatable types will be allocated on the heap instead of being stored in the small buffer.
The global typedef seq::r_any
and seq::r_nh_any
are defined to provide relocatable alternatives to seq::any
and seq::nh_any
.
Note that some containers (seq::tiered_vector
, seq::devector
, seq::flat_set/map
) are faster with relocatable types. Furthermore, seq::cvector
only works with relocatable types.
In some situations, a relocatable hold_any will be faster than a standard hold_any as its move copy/assignement operators are faster, as well as its swap member. For instance, a seq::flat_set
of seq::r_any
is WAY faster than for seq::any
.
hold_any
supports holding move only types like std::unique_ptr. In this case, the hold_any
will silently become a move only object, and trying to copy the hold_any
object will throw a std::bad_function_call exception.
Likewise, the hold_any
can only be casted to a reference to this type.
Example:
// create any from unique_ptr
seq::any a = std::unique_ptr<int>(new int(3));
// retrieve a reference to the unique_ptr
std::unique_ptr<int>& val1 = a.cast< std::unique_ptr<int>&>();
std::cout << *val1 << std::endl;
// move any object
seq::any b = std::move(a);
std::unique_ptr<int>& val2 = b.cast< std::unique_ptr<int>&>();
std::cout << *val2 << std::endl;
// try to copy: throw std::bad_function_call as copy operator is disabled for unique_ptr
try {
seq::any c = b;
}
catch (const std::bad_function_call&)
{
std::cout << "cannot copy any object containing a unique_ptr" << std::endl;
}
hold_any
interface is easily extendable to provide additional members or modify its standard behavior.
Below is an example of extension that adds the info() member returning a std::type_info
object and modify the hash function:
using namespace seq;
// Define a new interface for hold_any
struct NewInterface
{
// The base type info class, must inherit seq::any_type_info virtually
struct type_info : virtual any_type_info
{
virtual const std::type_info& info() const = 0;
};
// Concrete implementation for given type, must inherit type_info and any_typed_type_info<T> (for base implementation of common functions)
template<class T>
struct typed_type_info : type_info, any_typed_type_info<T>
{
// implementation of additional info() member
virtual const std::type_info& info() const { return typeid(T); }
// new implementation for the hash function based on the old one
virtual size_t hash_any(const void* in) const {
return any_typed_type_info<T>{}.hash_any(in)* UINT64_C(0xc4ceb9fe1a85ec53);
}
};
// Extended interface to seq::hold_any
template<class Base>
struct any_interface : Base
{
//add function to `hold_any` interface
const std::type_info& info() const { return this->type()->info(); }
};
};
using my_any = hold_any< NewInterface>;
my_any a = 2;
// print the type name
std::cout<< a.info().name() << std::endl;
// print the hash value
std::cout<< a.hash() << std::endl;
Below is a more complex example that transform hold_any
into a std::function
equivalent:
// for std::plus and std::multiplies (C++14)
#include <functional>
#include <seq/any.hpp>
// for seq::is_invocable
#include <seq/type_traits.hpp>
namespace seq
{
template<class R, class T, class... As>
SEQ_ALWAYS_INLINE typename std::enable_if< is_invocable<T, As ...>::value,R>::type call_fun(const T & fun, As... as){
return fun(std::forward<As>(as)...);
}
template<class R, class T, class... As>
SEQ_ALWAYS_INLINE typename std::enable_if< !is_invocable<T, As ...>::value,R>::type call_fun(const T & fun, As... as){
(void)fun;
throw std::bad_function_call(); return R();
}
template<class Fun>
struct FunInterface;
// Provide custom interface to hold_any in order to be callable like std::function
template<class R, class... As>
struct FunInterface< R(As...)>
{
// The base type info class, must inherit seq::any_type_info virtually.
struct type_info : virtual any_type_info
{
// Add virtual members() call and target_type()
virtual R call(const void* data, As... as) const = 0;
virtual const std::type_info& target_type() const = 0;
};
// Concrete implementation for given type, must inherit type_info and any_typed_type_info<T> (for base implementation of common functions)
template<class T>
struct typed_type_info : type_info, any_typed_type_info<T, false> //disable hashing support
{
virtual R call(const void* data, As... as) const override
{
// Make sure that this interface is still suitable for non invocable types
return call_fun<R>(* static_cast<const T*>(data), std::forward<As>(as)...);
}
virtual const std::type_info& target_type() const override
{
return typeid(T);
}
};
// Extended interface to seq::hold_any
template<class Base>
struct any_interface : Base
{
//add operator() to hold_any interface
R operator()(As... as) const
{
if (this->empty())
throw std::bad_function_call();
return this->type()->call(this->data(), std::forward<As>(as)...);
}
//add member target_type() to hold_any interface
const std::type_info& target_type() const
{
return this->empty() ? typeid(void) : this->type()->target_type();
}
};
};
// Define the seq::function type
template<class Signature>
using function = hold_any< FunInterface< Signature> >;
}
// Divide function
int divide(int a, int b) {return a / b;}
// Usage
seq::function<int(int,int)> plus_fun = std::plus<int>{}; // affect functor
seq::function<int(int,int)> minus_fun = [](int a, int b){return a - b;} ; // affect lambda
seq::function<int(int,int)> multiplies_fun = std::multiplies<int>{}; // affect functor
seq::function<int(int,int)> divide_fun = divide; // affect function
assert( plus_fun(1,2) == 3);
assert( minus_fun(2,1) == 1);
assert( multiplies_fun(2,3) == 6);
assert( divide_fun(9,3) == 3);