C++
Type effacement
Recherche…
Introduction
L'effacement de type est un ensemble de techniques permettant de créer un type pouvant fournir une interface uniforme à différents types sous-jacents, tout en masquant les informations de type sous-jacentes au client. std::function<R(A...)>
, qui a la capacité de contenir des objets appelables de différents types, est peut-être l'exemple le plus connu d'effacement de type en C ++.
Mécanisme de base
L'effacement de type permet de masquer le type d'un objet à l'aide du code, même s'il n'est pas dérivé d'une classe de base commune. Ce faisant, il fournit un pont entre les mondes du polymorphisme statique (modèles; au lieu d'utilisation, le type exact doit être connu au moment de la compilation, mais il n'a pas besoin d'être déclaré conforme à une interface à la définition) et le polymorphisme dynamique (héritage et fonctions virtuelles; sur le lieu d'utilisation, le type exact n'a pas besoin d'être connu au moment de la compilation, mais doit être déclaré conforme à une interface à la définition).
Le code suivant montre le mécanisme de base de type effacement.
#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;
};
Sur le site d'utilisation, seule la définition ci-dessus doit être visible, comme pour les classes de base avec des fonctions virtuelles. Par exemple:
#include <iostream>
void print_value(Printable const &p)
{
p.print(std::cout);
}
Notez qu'il ne s'agit pas d' un modèle, mais d'une fonction normale qui doit uniquement être déclarée dans un fichier d'en-tête et qui peut être définie dans un fichier d'implémentation (contrairement aux modèles dont la définition doit être visible sur le lieu d'utilisation).
A la définition du type concret, il n’ya rien à savoir sur Printable
, il suffit de se conformer à une interface, comme avec les templates:
struct MyType { int i; };
ostream& operator << (ostream &os, MyType const &mc)
{
return os << "MyType {" << mc.i << "}";
}
On peut maintenant passer un objet de cette classe à la fonction définie ci-dessus:
MyType foo = { 42 };
print_value(foo);
Effacement à un type régulier avec une vtable manuelle
C ++ prospère sur ce que l'on appelle un type régulier (ou du moins pseudo-régulier).
Un type Regular est un type qui peut être construit, assigné et attribué par copie ou par déplacement, peut être détruit et peut être comparé à une valeur égale. Il peut également être construit sans arguments. Enfin, il prend également en charge quelques autres opérations très utiles dans divers algorithmes et conteneurs std
.
Ceci est le document racine , mais en C ++ 11 voudrait ajouter le support std::hash
.
Je vais utiliser l'approche vtable manuelle pour taper l'effacement ici.
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 tel type régulier peut être utilisé comme clé pour un std::map
ou un std::unordered_map
qui accepte tout ce qui est normal pour une clé, comme:
std::map<regular_type, std::any>
serait fondamentalement une carte de rien régulier, à tout ce qui peut être copié.
Contrairement à any
, my regular_type
ne fait pas d’optimisation de petits objets et ne permet pas de récupérer les données d’origine. Obtenir le type original n'est pas difficile.
L'optimisation des petits objets nécessite que nous regular_type
un tampon de stockage aligné dans le type regular_type
, et regular_type
soigneusement le paramètre de suppression du ptr
pour ne détruire que l'objet et non le supprimer.
Je commencerais par make_dtor_unique_ptr
et je lui apprendrais à stocker parfois les données dans un tampon, puis dans le tas s'il n'y a pas de place dans le tampon. Cela peut être suffisant.
Un simple `std :: function`
std::function
efface quelques opérations. Une des choses qu'il faut, c'est que la valeur stockée soit copiable.
Cela pose des problèmes dans quelques contextes, tels que les lambda stockant des ptrs uniques. Si vous utilisez la std::function
dans un contexte où la copie importe peu, comme un pool de threads où vous envoyez des tâches à des threads, cette exigence peut entraîner une surcharge.
En particulier, std::packaged_task<Sig>
est un objet appelable qui est en déplacement uniquement. Vous pouvez stocker un std::packaged_task<R(Args...)>
dans un std::packaged_task<void(Args...)>
, mais c'est un moyen assez lourd et obscur de créer un mouvement uniquement classe de type effaçable.
Ainsi la task
. Cela montre comment vous pouvez écrire un simple type std::function
. J'ai omis le constructeur de copie (ce qui impliquerait également d'ajouter une méthode de clone
à 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());
}
};
Pour rendre cette bibliothèque digne de ce nom, vous voudrez ajouter une petite optimisation de mémoire tampon, afin de ne pas stocker tous les appels sur le tas.
L'ajout de SBO nécessiterait une task(task&&)
par défaut task(task&&)
, certains std::aligned_storage_t
dans la classe, un m_pImpl
unique_ptr
avec un paramètre qui peut être défini pour détruire uniquement (et ne pas renvoyer la mémoire au tas), et un emplace_move_to( void* ) = 0
dans la task_pimpl
.
exemple en direct du code ci-dessus (sans SBO).
Effacement à un tampon contigu de T
Tout effacement de type n'implique pas d'héritage virtuel, d'allocations, de nouveaux emplacements ou même de pointeurs de fonctions.
Ce qui rend l'effacement de type effacement de type, c'est qu'il décrit un (ensemble de) comportement (s), et prend tout type qui prend en charge ce comportement et l'enveloppe. Toutes les informations qui ne figurent pas dans cet ensemble de comportements sont "oubliées" ou "effacées".
Un array_view
prend sa plage entrante ou son type de conteneur et efface tout sauf le fait qu'il s'agit d'un tampon contigu de 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
prend tout conteneur qui prend en charge .data()
renvoyant un pointeur sur T
et une méthode .size()
, ou un tableau, et l'efface pour devenir une plage d'accès aléatoire sur les T
contigus.
Il peut prendre un std::vector<T>
, un std::string<T>
un std::array<T, N>
un T[37]
, une liste d'initialiseurs (y compris ceux basés sur {}
), ou quelque chose d'autre vous T* x.data()
qui le supporte (via T* x.data()
et size_t x.size()
).
Dans ce cas, les données que nous pouvons extraire de la chose que nous effaçons, ainsi que notre état non propriétaire "view", signifient que nous n'avons pas besoin d'allouer de la mémoire ou d'écrire des fonctions personnalisées dépendant du type.
Une amélioration consisterait à utiliser des data
non membres et une size
non membre dans un contexte compatible ADL.
Type effacement type effacement avec std :: any
Cet exemple utilise C ++ 14 et boost::any
. Dans C ++ 17, vous pouvez échanger std::any
place.
La syntaxe avec laquelle nous nous retrouvons est la suivante:
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);
ce qui est presque optimal.
Cet exemple est basé sur le travail de @dyp et @cpplearner ainsi que sur le mien.
Nous utilisons d'abord un tag pour faire circuler les types:
template<class T>struct tag_t{constexpr tag_t(){};};
template<class T>constexpr tag_t<T> tag{};
Cette classe de trait récupère la signature stockée avec any_method
:
Cela crée un type de pointeur de fonction et une fabrique pour les pointeurs de ces fonctions, avec une 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
est le type d'un pointeur de fonction que nous allons stocker avec l'instance. any_method_function::operator()
prend un tag_t<T>
et écrit une instance personnalisée du any_method_function::type
qui suppose que le any&
va être un T
Nous voulons pouvoir effacer plus d'une méthode à la fois. Nous les regroupons donc dans un tuple et écrivons un wrapper helper pour coller le tuple au stockage statique par type et y placer un pointeur.
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 );
}
};
Nous pourrions nous spécialiser dans un cas où la vtable est petite (par exemple, 1 élément) et utiliser des pointeurs directs stockés en classe dans ces cas pour des raisons d’efficacité.
Maintenant, nous commençons le super_any
. J'utilise super_any_t
pour rendre la déclaration de super_any
un peu plus facile.
template<class...methods>
struct super_any_t;
Ceci recherche les méthodes que le super supporte pour SFINAE et les meilleurs messages d'erreur:
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> ) )
{};
Ensuite, nous créons le type any_method
. Une any_method
est un pseudo-méthode-pointeur. Nous créons globalement et const
ment en utilisant une syntaxe:
const auto print=make_any_method( [](auto&&self, auto&&os){ os << self; } );
ou en C ++ 17:
const any_method print=[](auto&&self, auto&&os){ os << self; };
Notez que l'utilisation d'un non-lambda peut rendre les choses plus poilues, car nous utilisons le type pour une étape de recherche. Cela peut être corrigé, mais rendrait cet exemple plus long qu’il ne l’est déjà. Donc, toujours initialiser une méthode à partir d'un lambda, ou d'un type paramétré sur un 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)...);
}
};
Une méthode d'usine, pas nécessaire en C ++ 17 Je crois:
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)};
}
C'est la any
augmentée. Il est à la fois un any
, et il porte autour d' un faisceau de pointeurs de fonctions de type effacement qui change chaque fois que le contenu any
fait:
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;
}
};
Comme nous stockons any_method
s en tant qu'objets const
, cela facilite la création d'un super_any
:
template<class...Ts>
using super_any = super_any_t< std::remove_cv_t<Ts>... >;
Code de test:
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);
}
Initialement posté ici dans une question et une réponse SO (et les personnes mentionnées ci-dessus ont aidé à la mise en œuvre).