2016-08-08 14 views
22

Angenommen, ich möchte löschen mit Typ löschen eingeben.Typ löschen löschen, `any` Fragen?

kann ich 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 dies einem std::any?

Es kann nicht "im Rohzustand" getan werden. Aber an dem Punkt, an dem wir einen std::any zuweisen/konstruieren, haben wir die Typinformation, die wir brauchen.

So in der Theorie, ein any ergänzt:

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 irgendwie automatisch einen Code erneut binden, so dass die oben genannte Art von Syntax funktionieren würde.

Idealerweise wäre es so knapp 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); 
} 

Jetzt kann ich halten, dies zu einem Typ, noch einigermaßen 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? Einfach?

+0

Tangential bezogen; In welcher Implementierung testen Sie dies? Verfügt eine der großen Standardbibliotheken über verfügbare Versionen? – TartanLlama

+1

Ich habe das Gefühl, dass ich, als ich in der Vergangenheit versucht habe, ähnliche Dinge zu tun, letztendlich erkannte, dass alles auf virtuelle Vorlagen zurückging und dass sie von der Sprache nicht erlaubt waren. Ich bin mir sicher, dass etwas möglich ist, aber sicherlich sind viele der besseren Lösungen aus diesem Grund nicht möglich. –

+0

@TartanLlama Testen? Du meinst das oder die Variante? Ich tat es nicht. Habe es gerade gegen 'boost :: variant' getan und einen Tippfehler gefunden. Zum größten Teil, abgesehen von kleinen Unterschieden und Syntaxhässlichkeiten, die C++ 17 aufräumt, wird das Testen von 'std :: any'-Lösungen gegen' boost :: any' ausreichen, um zumindest auf einem Online-Compiler sicher zu sein. – Yakk

Antwort

7

Dies ist eine Lösung, die C++ 14 und boost::any verwendet, da ich keinen C++ 17-Compiler habe.

Die Syntax wir am Ende ist:

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); 

die fast optimal ist. Mit dem, was ich glaube, einfache C++ 17 Änderungen sein, sollte es wie folgt 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 ich dies verbessern würde durch eine auto*... von Zeigern auf any_method anstelle des decltype Lärm nehmen.

öffentlich von any Vererben ist ein bisschen riskant, als ob jemand die any aus der Spitze nimmt und modifiziert sie, die tuple von any_method_data veraltet sein wird. Wahrscheinlich sollten wir nur die gesamte any Schnittstelle nachahmen anstatt öffentlich zu erben.

@dyp schrieb einen Proof of Concept in Kommentaren zum OP. Dies basiert auf seiner Arbeit, aufgeräumt mit Wert-Semantik (gestohlen von boost::any) hinzugefügt. @ cpplearner's zeiger-basierte Lösung wurde verwendet, um es zu verkürzen (Danke!), und dann habe ich die Vtable-Optimierung hinzugefügt.


Zuerst verwenden wir einen Tag um Arten zu passieren:

template<class T>struct tag_t{constexpr tag_t(){};}; 
template<class T>constexpr tag_t<T> tag{}; 

Diese Eigenschaft Klasse die Signatur mit einem any_method gespeichert wird:

Diese eine Art Funktionszeiger erzeugt, und eine Fabrik für besagte Funktionszeiger, gegeben durch einen 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)...); 
    }; 
    } 
}; 

Nun wollen wir in unserer super_any keinen Funktionszeiger pro Operation speichern. So bündeln wir die Funktionszeiger in eine V-Tabelle nach oben:

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); 
    } 
}; 

konnten wir diese spezialisieren für einen Fall, in denen die V-Tabelle klein ist (zum Beispiel 1 Artikel), und direkt in seiner Klasse in den Fällen gespeicherten Zeiger verwenden für Effizienz.

Jetzt starten wir die super_any. Ich verwende super_any_t, um die Deklaration von super_any etwas einfacher zu machen.

template<class...methods> 
struct super_any_t; 

Dies sucht die Methoden, die die Super alle Träger für SFINAE:

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 Zeiger pseudo-Methode ist, wie print, dass wir global und const ly erstellen.

Wir speichern das Objekt, das wir bauen, mit innerhalb der any_method. Beachten Sie, dass wenn Sie es mit einem Nicht-Lambda konstruieren, Dinge haarig werden können, da der Typ dieses any_method als Teil des Dispatch-Mechanismus verwendet wird.

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)...); 
    } 
}; 

Ein Factory-Methode, nicht in 17 ++ C benötigt Ich glaube:

template<class Sig, class F> 
any_method<Sig, std::decay_t<F>> 
make_any_method(F&& f) { 
    return {std::forward<F>(f)}; 
} 

Dies ist die Augmented any. Es ist sowohl ein any, und es trägt um ein Bündel von Typ-Löschfunktionszeiger, die immer dann, wenn die darin enthaltenen any ändert:

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>(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>(t); 
    using dT=std::decay_t<T>; 
    this->change_type(tag<dT>); 
    return *this; 
    } 
}; 

Weil wir speichern die any_method s als const Objekte, macht dieses ein super_any macht ein bisschen einfacher:

template<class...Ts> 
using super_any = super_any_t< std::remove_const_t<std::remove_reference_t<Ts>>... >; 

Prüfregeln:

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 example.

Die Fehlermeldung, wenn ich versuche, eine nicht druckbare zu speichern struct X{}; innerhalb des super_any scheint zumindest auf Klirren 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, sobald Sie versuchen, den X{} in die zuweisen super_any<decltype(x0)>.

Die Struktur der any_method ist ausreichend kompatibel mit der pseudo_method, die ähnlich auf Varianten wirkt, die sie wahrscheinlich zusammengeführt werden können.


benutzte ich ein Handbuch VTable hier den Typ Löschoverhead 1 Zeiger zu halten pro super_any. Dadurch werden jedem Aufruf von any_method Umleitungskosten hinzugefügt. Wir könnten die Zeiger direkt in super_any sehr einfach speichern, und es wäre nicht schwer, diesen Parameter zu super_any zu machen. Auf jeden Fall sollten wir es im Fall der gelöschten Methode nur direkt speichern.


Zwei verschiedene any_method s desselben Typs (beispielsweise sowohl einen Funktionszeiger enthält) Laichen die gleiche Art von super_any. Dies verursacht Probleme beim Nachschlagen.

Unterscheiden ist ein bisschen schwierig. Wenn wir die super_any ändern, um auto* any_method zu nehmen, könnten wir alle identischen any_method s im vtable-Tupel bündeln und dann eine lineare Suche nach einem passenden Zeiger durchführen, wenn es mehr als 1 gibt. Die lineare Suche sollte entfernt werden der Compiler, es sei denn, Sie tun etwas Verrücktes wie die Weitergabe einer Referenz oder eines Zeigers an welche bestimmte any_method wir verwenden.

Das scheint jedoch den Rahmen dieser Antwort zu überschreiten; Die Existenz dieser Verbesserung ist für jetzt genug.


Zusätzlich wird ein ->*, die einen Zeiger (oder sogar Referenz!) Auf der linken Seite nimmt hinzugefügt werden kann, ist es diese lassen erkennen und das auch an das Lambda passieren. Dies kann es wirklich zu einer "beliebigen Methode" machen, indem es mit dieser Methode auf Varianten, Super_Anys und Zeigern arbeitet.

Mit ein bisschen von if constexpr Arbeit, kann der Lambda in jedem Fall auf einen ADL oder einen Methodenaufruf verzweigen.

Dies 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 zu tun“ (was den Wert std::cout << füttert).

+0

Ich denke, es sollte möglich sein, virtuelle Funktionen anstelle der Funktionszeiger zu verwenden, indem ein neuer Typ konstruiert wird, der von Klassenvorlagen-Instanziierungen abgeleitet ist, die den notwendigen Code innerhalb von 'super_any_t :: set_operation_to' erzeugen. Bei Mehrfachvererbung sollte das sogar etwa so kurz sein wie die Zuweisung von Funktionszeigern. Da Sie die Eingabefunktionen auf statusless/pure beschränken, scheint es möglich, diese Zeiger einmal pro * Liste der Eingabefunktionstypen * in einer vtable zu speichern. – dyp

+0

"da ich keinen C++ 17 Compiler habe." Du meinst abgesehen von Wandbox und anderen Online-Compilern? Auch: apt.llvm.org. – TemplateRex

+2

@TemplateRex Ich denke, Yakk meinte, dass keiner der Compiler voll unterstützt C++ 17 hier automatische Template-Parameter ... –

6

Hier ist meine Lösung. Es sieht kürzer aus als Yakk, und es verwendet std::aligned_storage und Placement neu nicht. Es unterstützt zusätzlich Stateful und Local Funktoren (was bedeutet, dass es niemals möglich sein könnte, super_any<&print> zu schreiben, da print eine lokale Variable sein könnte).

any_method:

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_any:

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>(t)) 
    , operations((OperationsToTypeErase::template invoker<ContainedType>)...) 
    {} 

    template<class T, class ContainedType = std::decay_t<T>> 
    super_any& operator=(T&& t) { 
    data = std::forward<T>(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)...); 
    }; 
} 

Usage:

#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

+0

Slick, mit der Signatur eines Funktionszeigers einschließlich des Typs der 'any_method', aber es beschränkt grundsätzlich Sie auf eindeutig typisierte' any_method's (mindestens auf dem gleichen Objekt). Ich sehe keine Möglichkeit, zu 'auto *' Pointer-Argumenten im 'super_any <&print>' zu migrieren. Ich vermute einen Hack mit einem 'auto *' - Tag. Ich denke, das ist ein Eckfall, da wir in der 'any_method'-Methode einen Kompilierungszeit-Polymorphismus benötigen, und' auto && self' in einem Lambda ist der einfachste Weg, dies zu tun. Viel kleiner als meins! (+1) – Yakk