C++
Digita la cancellazione
Ricerca…
introduzione
La cancellazione dei tipi è un insieme di tecniche per la creazione di un tipo che può fornire un'interfaccia uniforme a vari tipi sottostanti, nascondendo al contempo le informazioni sul tipo sottostante dal client. std::function<R(A...)>
, che ha la capacità di contenere oggetti chiamabili di vario tipo, è forse l'esempio più noto di cancellazione di tipo in C ++.
Meccanismo di base
Digitare la cancellazione è un modo per nascondere il tipo di un oggetto dal codice che lo utilizza, anche se non è derivato da una classe base comune. In tal modo, fornisce un ponte tra i mondi del polimorfismo statico (modelli, sul luogo di utilizzo, il tipo esatto deve essere noto al momento della compilazione, ma non è necessario dichiararlo conforme a un'interfaccia alla definizione) e polimorfismo dinamico (ereditarietà e funzioni virtuali: nel luogo di utilizzo, il tipo esatto non deve essere conosciuto al momento della compilazione, ma deve essere dichiarato conforme a un'interfaccia alla definizione).
Il codice seguente mostra il meccanismo di base della cancellazione dei tipi.
#include <ostream>
class Printable
{
public:
template <typename T>
Printable(T value) : pValue(new Value<T>(value)) {}
~Printable() { delete pValue; }
void print(std::ostream &os) const { pValue->print(os); }
private:
Printable(Printable const &) /* in C++1x: =delete */; // not implemented
void operator = (Printable const &) /* in C++1x: =delete */; // not implemented
struct ValueBase
{
virtual ~ValueBase() = default;
virtual void print(std::ostream &) const = 0;
};
template <typename T>
struct Value : ValueBase
{
Value(T const &t) : v(t) {}
virtual void print(std::ostream &os) const { os << v; }
T v;
};
ValueBase *pValue;
};
Nel sito di utilizzo, solo la definizione di cui sopra deve essere visibile, proprio come con le classi di base con funzioni virtuali. Per esempio:
#include <iostream>
void print_value(Printable const &p)
{
p.print(std::cout);
}
Si noti che questo non è un modello, ma una funzione normale che deve essere dichiarata solo in un file di intestazione e che può essere definita in un file di implementazione (diversamente dai modelli, la cui definizione deve essere visibile nel luogo di utilizzo).
Alla definizione del tipo concreto, non è necessario conoscere nulla di Printable
, deve solo essere conforme all'interfaccia, come con i modelli:
struct MyType { int i; };
ostream& operator << (ostream &os, MyType const &mc)
{
return os << "MyType {" << mc.i << "}";
}
Ora possiamo passare un oggetto di questa classe alla funzione definita sopra:
MyType foo = { 42 };
print_value(foo);
Cancellazione fino a un tipo normale con vtable manuale
Il C ++ prospera su ciò che è conosciuto come un tipo Regolare (o almeno Pseudo-Regolare).
Un tipo normale è un tipo che può essere costruito e assegnato a e assegnato da tramite copia o sposta, può essere distrutto e può essere paragonato allo stesso modo. Può anche essere costruito senza argomenti. Infine, ha anche il supporto per alcune altre operazioni che sono molto utili in vari algoritmi e contenitori std
.
Questo è il documento principale , ma in C ++ 11 vorrebbe aggiungere il supporto std::hash
.
Userò l'approccio manuale vtable per digitare la cancellazione qui.
using dtor_unique_ptr = std::unique_ptr<void, void(*)(void*)>;
template<class T, class...Args>
dtor_unique_ptr make_dtor_unique_ptr( Args&&... args ) {
return {new T(std::forward<Args>(args)...), [](void* self){ delete static_cast<T*>(self); }};
}
struct regular_vtable {
void(*copy_assign)(void* dest, void const* src); // T&=(T const&)
void(*move_assign)(void* dest, void* src); // T&=(T&&)
bool(*equals)(void const* lhs, void const* rhs); // T const&==T const&
bool(*order)(void const* lhs, void const* rhs); // std::less<T>{}(T const&, T const&)
std::size_t(*hash)(void const* self); // std::hash<T>{}(T const&)
std::type_info const&(*type)(); // typeid(T)
dtor_unique_ptr(*clone)(void const* self); // T(T const&)
};
template<class T>
regular_vtable make_regular_vtable() noexcept {
return {
[](void* dest, void const* src){ *static_cast<T*>(dest) = *static_cast<T const*>(src); },
[](void* dest, void* src){ *static_cast<T*>(dest) = std::move(*static_cast<T*>(src)); },
[](void const* lhs, void const* rhs){ return *static_cast<T const*>(lhs) == *static_cast<T const*>(rhs); },
[](void const* lhs, void const* rhs) { return std::less<T>{}(*static_cast<T const*>(lhs),*static_cast<T const*>(rhs)); },
[](void const* self){ return std::hash<T>{}(*static_cast<T const*>(self)); },
[]()->decltype(auto){ return typeid(T); },
[](void const* self){ return make_dtor_unique_ptr<T>(*static_cast<T const*>(self)); }
};
}
template<class T>
regular_vtable const* get_regular_vtable() noexcept {
static const regular_vtable vtable=make_regular_vtable<T>();
return &vtable;
}
struct regular_type {
using self=regular_type;
regular_vtable const* vtable = 0;
dtor_unique_ptr ptr{nullptr, [](void*){}};
bool empty() const { return !vtable; }
template<class T, class...Args>
void emplace( Args&&... args ) {
ptr = make_dtor_unique_ptr<T>(std::forward<Args>(args)...);
if (ptr)
vtable = get_regular_vtable<T>();
else
vtable = nullptr;
}
friend bool operator==(regular_type const& lhs, regular_type const& rhs) {
if (lhs.vtable != rhs.vtable) return false;
return lhs.vtable->equals( lhs.ptr.get(), rhs.ptr.get() );
}
bool before(regular_type const& rhs) const {
auto const& lhs = *this;
if (!lhs.vtable || !rhs.vtable)
return std::less<regular_vtable const*>{}(lhs.vtable,rhs.vtable);
if (lhs.vtable != rhs.vtable)
return lhs.vtable->type().before(rhs.vtable->type());
return lhs.vtable->order( lhs.ptr.get(), rhs.ptr.get() );
}
// technically friend bool operator< that calls before is also required
std::type_info const* type() const {
if (!vtable) return nullptr;
return &vtable->type();
}
regular_type(regular_type&& o):
vtable(o.vtable),
ptr(std::move(o.ptr))
{
o.vtable = nullptr;
}
friend void swap(regular_type& lhs, regular_type& rhs){
std::swap(lhs.ptr, rhs.ptr);
std::swap(lhs.vtable, rhs.vtable);
}
regular_type& operator=(regular_type&& o) {
if (o.vtable == vtable) {
vtable->move_assign(ptr.get(), o.ptr.get());
return *this;
}
auto tmp = std::move(o);
swap(*this, tmp);
return *this;
}
regular_type(regular_type const& o):
vtable(o.vtable),
ptr(o.vtable?o.vtable->clone(o.ptr.get()):dtor_unique_ptr{nullptr, [](void*){}})
{
if (!ptr && vtable) vtable = nullptr;
}
regular_type& operator=(regular_type const& o) {
if (o.vtable == vtable) {
vtable->copy_assign(ptr.get(), o.ptr.get());
return *this;
}
auto tmp = o;
swap(*this, tmp);
return *this;
}
std::size_t hash() const {
if (!vtable) return 0;
return vtable->hash(ptr.get());
}
template<class T,
std::enable_if_t< !std::is_same<std::decay_t<T>, regular_type>{}, int>* =nullptr
>
regular_type(T&& t) {
emplace<std::decay_t<T>>(std::forward<T>(t));
}
};
namespace std {
template<>
struct hash<regular_type> {
std::size_t operator()( regular_type const& r )const {
return r.hash();
}
};
template<>
struct less<regular_type> {
bool operator()( regular_type const& lhs, regular_type const& rhs ) const {
return lhs.before(rhs);
}
};
}
Un tipo così regolare può essere usato come chiave per una std::map
o una std::unordered_map
che accetta qualsiasi cosa normale per una chiave, come:
std::map<regular_type, std::any>
sarebbe fondamentalmente una mappa da regolare normale, a tutto ciò che è possibile copiare.
Diversamente da any
, il mio regular_type
non ottimizza gli oggetti di piccole dimensioni e non supporta il recupero dei dati originali. Ottenere il tipo originale non è difficile.
L'ottimizzazione degli oggetti di piccole dimensioni richiede che noi archiviamo un buffer di archiviazione allineato all'interno del regular_type
, e regular_type
attentamente il deleter del ptr
per distruggere solo l'oggetto e non cancellarlo.
Vorrei iniziare a make_dtor_unique_ptr
e insegnargli come memorizzare i dati a volte in un buffer e quindi nell'heap se non c'è spazio nel buffer. Potrebbe essere sufficiente.
Un solo spostamento `std :: function`
std::function
tipo di std::function
cancella fino a poche operazioni. Una delle cose che richiede è che il valore memorizzato sia copiabile.
Ciò causa problemi in alcuni contesti, come lambdas che memorizza ptr unici. Se si utilizza la std::function
in un contesto in cui la copia non è importante, come un pool di thread in cui si inviano attività ai thread, questo requisito può aggiungere un sovraccarico.
In particolare, std::packaged_task<Sig>
è un oggetto callable che è solo move. Puoi archiviare un file std::packaged_task<R(Args...)>
in un file std::packaged_task<void(Args...)>
, ma questo è un modo abbastanza pesante e oscuro per creare un solo spostamento callable type-erasure class.
Quindi il task
. Questo dimostra come potresti scrivere un semplice tipo di std::function
. Ho omesso il costruttore di copie (che implicherebbe l'aggiunta di un metodo clone
a details::task_pimpl<...>
).
template<class Sig>
struct task;
// putting it in a namespace allows us to specialize it nicely for void return value:
namespace details {
template<class R, class...Args>
struct task_pimpl {
virtual R invoke(Args&&...args) const = 0;
virtual ~task_pimpl() {};
virtual const std::type_info& target_type() const = 0;
};
// store an F. invoke(Args&&...) calls the f
template<class F, class R, class...Args>
struct task_pimpl_impl:task_pimpl<R,Args...> {
F f;
template<class Fin>
task_pimpl_impl( Fin&& fin ):f(std::forward<Fin>(fin)) {}
virtual R invoke(Args&&...args) const final override {
return f(std::forward<Args>(args)...);
}
virtual const std::type_info& target_type() const final override {
return typeid(F);
}
};
// the void version discards the return value of f:
template<class F, class...Args>
struct task_pimpl_impl<F,void,Args...>:task_pimpl<void,Args...> {
F f;
template<class Fin>
task_pimpl_impl( Fin&& fin ):f(std::forward<Fin>(fin)) {}
virtual void invoke(Args&&...args) const final override {
f(std::forward<Args>(args)...);
}
virtual const std::type_info& target_type() const final override {
return typeid(F);
}
};
};
template<class R, class...Args>
struct task<R(Args...)> {
// semi-regular:
task()=default;
task(task&&)=default;
// no copy
private:
// aliases to make some SFINAE code below less ugly:
template<class F>
using call_r = std::result_of_t<F const&(Args...)>;
template<class F>
using is_task = std::is_same<std::decay_t<F>, task>;
public:
// can be constructed from a callable F
template<class F,
// that can be invoked with Args... and converted-to-R:
class= decltype( (R)(std::declval<call_r<F>>()) ),
// and is not this same type:
std::enable_if_t<!is_task<F>{}, int>* = nullptr
>
task(F&& f):
m_pImpl( make_pimpl(std::forward<F>(f)) )
{}
// the meat: the call operator
R operator()(Args... args)const {
return m_pImpl->invoke( std::forward<Args>(args)... );
}
explicit operator bool() const {
return (bool)m_pImpl;
}
void swap( task& o ) {
std::swap( m_pImpl, o.m_pImpl );
}
template<class F>
void assign( F&& f ) {
m_pImpl = make_pimpl(std::forward<F>(f));
}
// Part of the std::function interface:
const std::type_info& target_type() const {
if (!*this) return typeid(void);
return m_pImpl->target_type();
}
template< class T >
T* target() {
return target_impl<T>();
}
template< class T >
const T* target() const {
return target_impl<T>();
}
// compare with nullptr :
friend bool operator==( std::nullptr_t, task const& self ) { return !self; }
friend bool operator==( task const& self, std::nullptr_t ) { return !self; }
friend bool operator!=( std::nullptr_t, task const& self ) { return !!self; }
friend bool operator!=( task const& self, std::nullptr_t ) { return !!self; }
private:
template<class T>
using pimpl_t = details::task_pimpl_impl<T, R, Args...>;
template<class F>
static auto make_pimpl( F&& f ) {
using dF=std::decay_t<F>;
using pImpl_t = pimpl_t<dF>;
return std::make_unique<pImpl_t>(std::forward<F>(f));
}
std::unique_ptr<details::task_pimpl<R,Args...>> m_pImpl;
template< class T >
T* target_impl() const {
return dynamic_cast<pimpl_t<T>*>(m_pImpl.get());
}
};
Per rendere questa libreria degna, dovresti aggiungere una piccola ottimizzazione del buffer, in modo che non memorizzi tutti i callable nell'heap.
L'aggiunta di SBO richiederebbe un'attività non predefinita task(task&&)
, alcuni std::aligned_storage_t
all'interno della classe, un m_pImpl
unique_ptr
con un deleter che può essere impostato su destroy-only (e non restituire la memoria emplace_move_to( void* ) = 0
) e un emplace_move_to( void* ) = 0
nel task_pimpl
.
esempio dal vivo del codice precedente (senza SBO).
Cancellando fino a un buffer contiguo di T
Non tutti i tipi di cancellazione includono ereditarietà virtuale, allocazioni, posizionamenti nuovi o persino puntatori di funzioni.
Ciò che rende la cancellazione del tipo cancellato è che descrive un (o più) comportamento (i), e accetta qualsiasi tipo che supporti quel comportamento e lo avvolge. Tutte le informazioni che non sono in quell'insieme di comportamenti sono "dimenticate" o "cancellate".
Un array_view
prende il suo intervallo o tipo di contenitore in entrata e cancella tutto tranne il fatto che si tratta di un buffer contiguo di T
// helper traits for SFINAE:
template<class T>
using data_t = decltype( std::declval<T>().data() );
template<class Src, class T>
using compatible_data = std::integral_constant<bool, std::is_same< data_t<Src>, T* >{} || std::is_same< data_t<Src>, std::remove_const_t<T>* >{}>;
template<class T>
struct array_view {
// the core of the class:
T* b=nullptr;
T* e=nullptr;
T* begin() const { return b; }
T* end() const { return e; }
// provide the expected methods of a good contiguous range:
T* data() const { return begin(); }
bool empty() const { return begin()==end(); }
std::size_t size() const { return end()-begin(); }
T& operator[](std::size_t i)const{ return begin()[i]; }
T& front()const{ return *begin(); }
T& back()const{ return *(end()-1); }
// useful helpers that let you generate other ranges from this one
// quickly and safely:
array_view without_front( std::size_t i=1 ) const {
i = (std::min)(i, size());
return {begin()+i, end()};
}
array_view without_back( std::size_t i=1 ) const {
i = (std::min)(i, size());
return {begin(), end()-i};
}
// array_view is plain old data, so default copy:
array_view(array_view const&)=default;
// generates a null, empty range:
array_view()=default;
// final constructor:
array_view(T* s, T* f):b(s),e(f) {}
// start and length is useful in my experience:
array_view(T* s, std::size_t length):array_view(s, s+length) {}
// SFINAE constructor that takes any .data() supporting container
// or other range in one fell swoop:
template<class Src,
std::enable_if_t< compatible_data<std::remove_reference_t<Src>&, T >{}, int>* =nullptr,
std::enable_if_t< !std::is_same<std::decay_t<Src>, array_view >{}, int>* =nullptr
>
array_view( Src&& src ):
array_view( src.data(), src.size() )
{}
// array constructor:
template<std::size_t N>
array_view( T(&arr)[N] ):array_view(arr, N) {}
// initializer list, allowing {} based:
template<class U,
std::enable_if_t< std::is_same<const U, T>{}, int>* =nullptr
>
array_view( std::initializer_list<U> il ):array_view(il.begin(), il.end()) {}
};
un array_view
prende qualsiasi contenitore che supporti .data()
restituendo un puntatore a T
e un metodo .size()
, o un array, e lo cancella come un intervallo di accesso casuale su T
contigui.
Può prendere uno std::vector<T>
, uno std::string<T>
uno std::array<T, N>
a T[37]
, un elenco inizializzatore (compresi quelli basati su {}
), o qualcos'altro si compone che lo supporta (tramite T* x.data()
e size_t x.size()
).
In questo caso, i dati che possiamo estrarre dalla cosa che stiamo cancellando, insieme al nostro stato di "non visualizzazione", significa che non dobbiamo allocare memoria o scrivere funzioni dipendenti dal tipo personalizzato.
Un miglioramento sarebbe usare un terzi data
e un terzo size
in un contesto ADL abilitato.
Digita cancellando il tipo cancellato con std :: any
Questo esempio utilizza C ++ 14 e boost::any
. In C ++ 17 puoi invece scambiare in std::any
.
La sintassi che otteniamo è:
const auto print =
make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });
super_any<decltype(print)> a = 7;
(a->*print)(std::cout);
che è quasi ottimale.
Questo esempio è basato sul lavoro di @dyp e @cpplearner e sul mio.
Per prima cosa utilizziamo un tag per passare tipi:
template<class T>struct tag_t{constexpr tag_t(){};};
template<class T>constexpr tag_t<T> tag{};
Questa classe di caratteristiche ottiene la firma memorizzata con any_method
:
Questo crea un tipo di puntatore a funzione e una factory per i detti puntatori di funzione, dato un any_method
:
template<class any_method>
using any_sig_from_method = typename any_method::signature;
template<class any_method, class Sig=any_sig_from_method<any_method>>
struct any_method_function;
template<class any_method, class R, class...Args>
struct any_method_function<any_method, R(Args...)>
{
template<class T>
using decorate = std::conditional_t< any_method::is_const, T const, T >;
using any = decorate<boost::any>;
using type = R(*)(any&, any_method const*, Args&&...);
template<class T>
type operator()( tag_t<T> )const{
return +[](any& self, any_method const* method, Args&&...args) {
return (*method)( boost::any_cast<decorate<T>&>(self), decltype(args)(args)... );
};
}
};
any_method_function::type
è il tipo di puntatore a funzione che memorizzeremo insieme all'istanza. any_method_function::operator()
prende un tag_t<T>
e scrive un'istanza personalizzata del any_method_function::type
che presuppone l' any&
sta per essere una T
Vogliamo essere in grado di digitare-cancellare più di un metodo alla volta. Quindi li raggruppiamo in una tupla e scriviamo un wrapper di supporto per incollare la tupla nello storage statico su base per tipo e mantenere un puntatore su di essi.
template<class...any_methods>
using any_method_tuple = std::tuple< typename any_method_function<any_methods>::type... >;
template<class...any_methods, class T>
any_method_tuple<any_methods...> make_vtable( tag_t<T> ) {
return std::make_tuple(
any_method_function<any_methods>{}(tag<T>)...
);
}
template<class...methods>
struct any_methods {
private:
any_method_tuple<methods...> const* vtable = 0;
template<class T>
static any_method_tuple<methods...> const* get_vtable( tag_t<T> ) {
static const auto table = make_vtable<methods...>(tag<T>);
return &table;
}
public:
any_methods() = default;
template<class T>
any_methods( tag_t<T> ): vtable(get_vtable(tag<T>)) {}
any_methods& operator=(any_methods const&)=default;
template<class T>
void change_type( tag_t<T> ={} ) { vtable = get_vtable(tag<T>); }
template<class any_method>
auto get_invoker( tag_t<any_method> ={} ) const {
return std::get<typename any_method_function<any_method>::type>( *vtable );
}
};
Potremmo specializzarlo in casi in cui il vtable è piccolo (ad esempio, 1 elemento) e utilizzare puntatori diretti memorizzati in classe in questi casi per l'efficienza.
Ora iniziamo il super_any
. Io uso super_any_t
per rendere la dichiarazione di super_any
un po 'più semplice.
template<class...methods>
struct super_any_t;
Questo ricerca i metodi che il super supporta per SFINAE e messaggi di errore migliori:
template<class super_any, class method>
struct super_method_applies_helper : std::false_type {};
template<class M0, class...Methods, class method>
struct super_method_applies_helper<super_any_t<M0, Methods...>, method> :
std::integral_constant<bool, std::is_same<M0, method>{} || super_method_applies_helper<super_any_t<Methods...>, method>{}>
{};
template<class...methods, class method>
auto super_method_test( super_any_t<methods...> const&, tag_t<method> )
{
return std::integral_constant<bool, super_method_applies_helper< super_any_t<methods...>, method >{} && method::is_const >{};
}
template<class...methods, class method>
auto super_method_test( super_any_t<methods...>&, tag_t<method> )
{
return std::integral_constant<bool, super_method_applies_helper< super_any_t<methods...>, method >{} >{};
}
template<class super_any, class method>
struct super_method_applies:
decltype( super_method_test( std::declval<super_any>(), tag<method> ) )
{};
Successivamente creiamo il tipo any_method
. Un any_method
è un puntatore pseudo-metodo. Lo creiamo a livello globale e const
sintassi come:
const auto print=make_any_method( [](auto&&self, auto&&os){ os << self; } );
o in C ++ 17:
const any_method print=[](auto&&self, auto&&os){ os << self; };
Nota che l'uso di un non-lambda può rendere le cose pelose, poiché utilizziamo il tipo per una fase di ricerca. Questo può essere risolto, ma renderebbe questo esempio più lungo di quanto non sia già. Quindi inizializza sempre un metodo qualsiasi da una lambda, o da un tipo parametarizzato su una lambda.
template<class Sig, bool const_method, class F>
struct any_method {
using signature=Sig;
enum{is_const=const_method};
private:
F f;
public:
template<class Any,
// SFINAE testing that one of the Anys's matches this type:
std::enable_if_t< super_method_applies< Any&&, any_method >{}, int>* =nullptr
>
friend auto operator->*( Any&& self, any_method const& m ) {
// we don't use the value of the any_method, because each any_method has
// a unique type (!) and we check that one of the auto*'s in the super_any
// already has a pointer to us. We then dispatch to the corresponding
// any_method_data...
return [&self, invoke = self.get_invoker(tag<any_method>), m](auto&&...args)->decltype(auto)
{
return invoke( decltype(self)(self), &m, decltype(args)(args)... );
};
}
any_method( F fin ):f(std::move(fin)) {}
template<class...Args>
decltype(auto) operator()(Args&&...args)const {
return f(std::forward<Args>(args)...);
}
};
Un metodo factory, non necessario in C ++ 17, credo:
template<class Sig, bool is_const=false, class F>
any_method<Sig, is_const, std::decay_t<F>>
make_any_method( F&& f ) {
return {std::forward<F>(f)};
}
Questo è il any
. E 'sia un any
, e svolge attorno ad un fascio di puntatori a funzione del tipo di cancellazione che cambia quando il contenuto any
fa:
template<class... methods>
struct super_any_t:boost::any, any_methods<methods...> {
using vtable=any_methods<methods...>;
public:
template<class T,
std::enable_if_t< !std::is_base_of<super_any_t, std::decay_t<T>>{}, int> =0
>
super_any_t( T&& t ):
boost::any( std::forward<T>(t) )
{
using dT=std::decay_t<T>;
this->change_type( tag<dT> );
}
boost::any& as_any()&{return *this;}
boost::any&& as_any()&&{return std::move(*this);}
boost::any const& as_any()const&{return *this;}
super_any_t()=default;
super_any_t(super_any_t&& o):
boost::any( std::move( o.as_any() ) ),
vtable(o)
{}
super_any_t(super_any_t const& o):
boost::any( o.as_any() ),
vtable(o)
{}
template<class S,
std::enable_if_t< std::is_same<std::decay_t<S>, super_any_t>{}, int> =0
>
super_any_t( S&& o ):
boost::any( std::forward<S>(o).as_any() ),
vtable(o)
{}
super_any_t& operator=(super_any_t&&)=default;
super_any_t& operator=(super_any_t const&)=default;
template<class T,
std::enable_if_t< !std::is_same<std::decay_t<T>, super_any_t>{}, int>* =nullptr
>
super_any_t& operator=( T&& t ) {
((boost::any&)*this) = std::forward<T>(t);
using dT=std::decay_t<T>;
this->change_type( tag<dT> );
return *this;
}
};
Poiché memorizziamo any_method
come oggetti const
, ciò rende la creazione di un super_any
un po 'più semplice:
template<class...Ts>
using super_any = super_any_t< std::remove_cv_t<Ts>... >;
Codice di prova:
const auto print = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });
const auto wprint = make_any_method<void(std::wostream&)>([](auto&& p, std::wostream& os ){ os << p << L"\n"; });
int main()
{
super_any<decltype(print), decltype(wprint)> a = 7;
super_any<decltype(print), decltype(wprint)> a2 = 7;
(a->*print)(std::cout);
(a->*wprint)(std::wcout);
}
Originariamente pubblicato qui in una domanda e risposta SO (e le persone indicate sopra hanno aiutato con l'implementazione).