Angenommen, ich möchte Erase mit Type Erase eingeben.
Ich kann Pseudo-Methoden für Varianten erstellen, die eine natürliche ermöglichen:
pseudo_method print = [](auto&& self, auto&& os){ os << self; };
std::variant<A,B,C> var = // create a variant of type A B or C
(var->*print)(std::cout); // print it out without knowing what it is
Meine Frage ist, wie erweitere ich das auf a std::any
?
Es kann nicht “im Rohzustand” gemacht werden. Aber an dem Punkt, an dem wir a zuweisen/konstruieren std::any
Wir haben die Typinformationen, die wir brauchen.
Theoretisch also ein Augmented any
:
template<class...OperationsToTypeErase>
struct super_any {
std::any data;
// or some transformation of OperationsToTypeErase?
std::tuple<OperationsToTypeErase...> operations;
// ?? what for ctor/assign/etc?
};
könnte irgendeinen Code irgendwie automatisch neu binden, so dass die obige Art von Syntax funktionieren würde.
Im Idealfall wäre es so kurz im Gebrauch wie der Variantenfall.
template<class...Ops, class Op,
// SFINAE filter that an op matches:
std::enable_if_t< std::disjunction< std::is_same<Ops, Op>... >{}, int>* =nullptr
>
decltype(auto) operator->*( super_any<Ops...>& a, any_method<Op> ) {
return std::get<Op>(a.operations)(a.data);
}
Kann ich das jetzt auf a belassen Typaber vernünftigerweise die Lambda-Syntax verwenden, um die Dinge einfach zu halten?
Idealerweise möchte ich:
any_method<void(std::ostream&)> print =
[](auto&& self, auto&& os){ os << self; };
using printable_any = make_super_any<&print>;
printable_any bob = 7; // sets up the printing data attached to the any
int main() {
(bob->*print)(std::cout); // prints 7
bob = 3.14159;
(bob->*print)(std::cout); // prints 3.14159
}
oder ähnliche Syntax. Ist das unmöglich? Undurchführbar? Leicht?
Dies ist eine Lösung, die C++14 und verwendet boost::any
da ich keinen C++17-Compiler habe.
Die Syntax, mit der wir enden, lautet:
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);
was fast optimal ist. Mit meiner Meinung nach einfachen C++17-Änderungen sollte es so aussehen:
constexpr any_method<void(std::ostream&)> print =
[](auto&& p, std::ostream& t){ t << p << "n"; };
super_any<&print> a = 7;
(a->*print)(std::cout);
In C ++ 17 würde ich dies verbessern, indem ich a nehme auto*...
von Zeigern auf any_method
anstatt des decltype
Lärm.
Öffentliches Erbe von any
ist ein bisschen riskant, als ob jemand das nimmt any
von oben und modifiziert es, die tuple
von any_method_data
wird veraltet sein. Wahrscheinlich sollten wir einfach das Ganze nachahmen any
Schnittstelle, anstatt öffentlich zu erben.
@dyp hat in Kommentaren zum OP einen Proof of Concept geschrieben. Dies basiert auf seiner Arbeit, bereinigt mit Wertesemantik (gestohlen aus boost::any
) hinzugefügt. Die zeigerbasierte Lösung von @cpplearner wurde verwendet, um es zu verkürzen (danke!), und dann habe ich die vtable-Optimierung darüber hinzugefügt.
Zuerst verwenden wir ein Tag, um Typen zu übergeben:
template<class T>struct tag_t{constexpr tag_t(){};};
template<class T>constexpr tag_t<T> tag{};
Diese Eigenschaftsklasse erhält die Signatur, die mit einem gespeichert wird any_method
:
Dies erzeugt einen Funktionszeigertyp und eine Fabrik für die Funktionszeiger, wenn an gegeben ist any_method
:
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...)>
{
using type = R(*)(boost::any&, any_method const*, Args...);
template<class T>
type operator()( tag_t<T> )const{
return [](boost::any& self, any_method const* method, Args...args) {
return (*method)( boost::any_cast<T&>(self), decltype(args)(args)... );
};
}
};
Jetzt wollen wir keinen Funktionszeiger pro Operation in unserem speichern super_any
. Also bündeln wir die Funktionszeiger in einer vtable:
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 );
}
};
Wir könnten dies auf Fälle spezialisieren, in denen die vtable klein ist (z. B. 1 Element), und in diesen Fällen aus Effizienzgründen direkte Zeiger verwenden, die in der Klasse gespeichert sind.
Jetzt starten wir die super_any
. ich benutze super_any_t
die Erklärung abzugeben super_any
etwas leichter.
template<class...methods>
struct super_any_t;
Dadurch werden die Methoden, die der Super Any unterstützt, nach SFINAE durchsucht:
template<class super_any, class method>
struct super_method_applies : std::false_type {};
template<class M0, class...Methods, class method>
struct super_method_applies<super_any_t<M0, Methods...>, method> :
std::integral_constant<bool, std::is_same<M0, method>{} || super_method_applies<super_any_t<Methods...>, method>{}>
{};
Dies ist der Pseudo-Methodenzeiger, wie print
die wir global erstellen und const
ly.
Wir speichern das Objekt, mit dem wir dies konstruieren, in der any_method
. Beachten Sie, dass, wenn Sie es mit einem Nicht-Lambda konstruieren, die Dinge haarig werden können, da die Typ von diesem any_method
wird als Teil des Versandmechanismus verwendet.
template<class Sig, class F>
struct any_method {
using signature=Sig;
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< std::decay_t<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)...);
}
};
Eine Fabrikmethode, die in C ++ 17 meiner Meinung nach nicht benötigt wird:
template<class Sig, class F>
any_method<Sig, std::decay_t<F>>
make_any_method( F&& f ) {
return {std::forward<F>(f)};
}
Das ist das Augmentierte any
. Es ist sowohl ein any
und es trägt ein Bündel von Typlöschungsfunktionszeigern mit sich herum, die sich ändern, wann immer die enthalten sind any
tut:
template<class... methods>
struct super_any_t:boost::any, any_methods<methods...> {
private:
template<class T>
T* get() { return boost::any_cast<T*>(this); }
public:
template<class T,
std::enable_if_t< !std::is_same<std::decay_t<T>, super_any_t>{}, int>* =nullptr
>
super_any_t( T&& t ):
boost::any( std::forward<T>
{
using dT=std::decay_t<T>;
this->change_type( tag<dT> );
}
super_any_t()=default;
super_any_t(super_any_t&&)=default;
super_any_t(super_any_t const&)=default;
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>
using dT=std::decay_t<T>;
this->change_type( tag<dT> );
return *this;
}
};
Denn wir speichern die any_method
ist wie const
Objekte, macht dies eine super_any
etwas einfacher:
template<class...Ts>
using super_any = super_any_t< std::remove_const_t<std::remove_reference_t<Ts>>... >;
Testcode:
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"; });
const auto wont_work = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "n"; });
struct X {};
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);
// (a->*wont_work)(std::cout);
double d = 4.2;
a = d;
(a->*print)(std::cout);
(a->*wprint)(std::wcout);
(a2->*print)(std::cout);
(a2->*wprint)(std::wcout);
// a = X{}; // generates an error if you try to store a non-printable
}
Live-Beispiel.
Die Fehlermeldung, wenn ich versuche, eine nicht druckbare Datei zu speichern struct X{};
innerhalb der super_any
scheint zumindest auf clang vernünftig:
main.cpp:150:87: error: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'X')
const auto x0 = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "n"; });
Dies geschieht in dem Moment, in dem Sie versuchen, die zuzuweisen X{}
in die super_any<decltype(x0)>
.
Die Struktur der any_method
ist ausreichend verträglich mit der pseudo_method
das wirkt ähnlich auf Varianten, dass sie wahrscheinlich zusammengeführt werden können.
Ich habe hier eine manuelle vtable verwendet, um den Aufwand für das Löschen von Typen auf 1 Zeiger pro zu halten super_any
. Dies fügt jedem Aufruf von any_method Umleitungskosten hinzu. Wir könnten die Zeiger direkt in speichern super_any
sehr einfach, und es wäre nicht schwer, dies zu einem Parameter zu machen super_any
. In jedem Fall sollten wir es im Fall der 1 gelöschten Methode einfach direkt speichern.
Zwei verschiedene any_method
s des gleichen Typs (sagen wir, beide enthalten einen Funktionszeiger) erzeugen die gleiche Art von super_any
. Dies führt zu Problemen bei der Suche.
Die Unterscheidung zwischen ihnen ist ein bisschen schwierig. Wenn wir die ändern super_any
nehmen auto* any_method
könnten wir alle identischen Typen bündeln any_method
s im vtable-Tupel, dann führen Sie eine lineare Suche nach einem übereinstimmenden Zeiger durch, wenn es mehr als 1 gibt. Die lineare Suche sollte vom Compiler wegoptimiert werden, es sei denn, Sie tun etwas Verrücktes wie das Übergeben einer Referenz oder eines Zeigers auf welches bestimmte any_method
wir benutzen.
Das scheint jedoch den Rahmen dieser Antwort zu sprengen; die Existenz dieser Verbesserung reicht für den Moment.
Außerdem ein ->*
Das nimmt einen Zeiger (oder sogar eine Referenz!) auf der linken Seite kann hinzugefügt werden, damit es dies erkennen und auch an das Lambda weitergeben kann. Dies kann es wirklich zu einer “beliebigen Methode” machen, da es mit dieser Methode auf Varianten, super_anys und Zeigern funktioniert.
Mit ein bisschen if constexpr
funktioniert, kann das Lambda in jedem Fall auf einen ADL- oder Methodenaufruf verzweigen.
Das sollte uns geben:
(7->*print)(std::cout);
((super_any<&print>)(7)->*print)(std::cout); // C++17 version of above syntax
((std::variant<int, double>{7})->*print)(std::cout);
int* ptr = new int(7);
(ptr->*print)(std::cout);
(std::make_unique<int>(7)->*print)(std::cout);
(std::make_shared<int>(7)->*print)(std::cout);
mit dem any_method
nur “das Richtige tun” (was den Wert füttert std::cout <<
).
Hier ist meine Lösung. Es sieht kürzer aus als das von Yakk und wird nicht verwendet std::aligned_storage
und Platzierung neu. Es unterstützt zusätzlich zustandsbehaftete und lokale Funktoren (was impliziert, dass es möglicherweise nie möglich ist, zu schreiben super_any<&print>
seit print
könnte eine lokale Variable sein).
beliebige_methode:
template<class F, class Sig> struct any_method;
template<class F, class Ret, class... Args> struct any_method<F,Ret(Args...)> {
F f;
template<class T>
static Ret invoker(any_method& self, boost::any& data, Args... args) {
return self.f(boost::any_cast<T&>(data), std::forward<Args>(args)...);
}
using invoker_type = Ret (any_method&, boost::any&, Args...);
};
make_any_method:
template<class Sig, class F>
any_method<std::decay_t<F>,Sig> make_any_method(F&& f) {
return { std::forward<F>(f) };
}
super_beliebig:
template<class...OperationsToTypeErase>
struct super_any {
boost::any data;
std::tuple<typename OperationsToTypeErase::invoker_type*...> operations = {};
template<class T, class ContainedType = std::decay_t<T>>
super_any(T&& t)
: data(std::forward<T>
, operations((OperationsToTypeErase::template invoker<ContainedType>)...)
{}
template<class T, class ContainedType = std::decay_t<T>>
super_any& operator=(T&& t) {
data = std::forward<T>
operations = { (OperationsToTypeErase::template invoker<ContainedType>)... };
return *this;
}
};
Operator->*:
template<class...Ops, class F, class Sig,
// SFINAE filter that an op matches:
std::enable_if_t< std::disjunction< std::is_same<Ops, any_method<F,Sig>>... >{}, int> = 0
>
auto operator->*( super_any<Ops...>& a, any_method<F,Sig> f) {
auto fptr = std::get<typename any_method<F,Sig>::invoker_type*>(a.operations);
return [fptr,f, &a](auto&&... args) mutable {
return fptr(f, a.data, std::forward<decltype(args)>(args)...);
};
}
Verwendung:
#include <iostream>
auto print = make_any_method<void(std::ostream&)>(
[](auto&& self, auto&& os){ os << self; }
);
using printable_any = super_any<decltype(print)>;
printable_any bob = 7; // sets up the printing data attached to the any
int main() {
(bob->*print)(std::cout); // prints 7
bob = 3.14159;
(bob->*print)(std::cout); // prints 3.14159
}
Live
.
Tangential verwandt; Gegen welche Implementierung testen Sie dies? Haben einige der wichtigsten stdlibs leicht verfügbare Versionen?
– TartanLama
8. August 16 um 18:47 Uhr
Ich habe das Gefühl, als ich in der Vergangenheit versucht habe, ähnliche Dinge zu tun, habe ich schließlich festgestellt, dass eigentlich alles auf virtuelle Vorlagen zurückgeht und dass dies von der Sprache nicht erlaubt ist. Ich bin sicher, dass einiges möglich ist, aber sicherlich sind viele der schöneren Lösungen aus diesem Grund unmöglich.
– Nir Friedman
8. August 16 um 19:12 Uhr
Ich bin mir nicht sicher, ob ich alle Teile zusammen habe, aber hier ist eine sehr grobe Skizze: coliru.stacked-crooked.com/a/2ab8d7e41d24e616
– dyp
8. August 16 um 20:46 Uhr
Leicht verfeinert mit Operatorüberladung: coliru.stacked-crooked.com/a/23a25da83c5ba11d
– dyp
8. August 16 um 20:57 Uhr
Als ich das Dokumentationsbeispiel mit Variante und Operator->* las, wusste ich, dass Sie es waren, ohne auf den Namen zu schauen
– Johannes Schaub – litb
9. August 16 um 7:58 Uhr