Typ löschen Typ löschen, “irgendwelche” Fragen?

Lesezeit: 16 Minuten

Typ loschen Typ loschen irgendwelche Fragen
Yakk – Adam Nevraumont

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?

  • 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

Typ loschen Typ loschen irgendwelche Fragen
Yakk – Adam Nevraumont

Dies ist eine Lösung, die C++14 und verwendet boost::anyda 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 printdie wir global erstellen und constly.

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 anyund 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_methodist 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_methods 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_methodkönnten wir alle identischen Typen bündeln any_methods 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 <<).

  • Ich denke, es sollte möglich sein, virtuelle Funktionen anstelle der Funktionszeiger zu verwenden, indem ein neuer Typ konstruiert wird, der von Instanziierungen von Klassenvorlagen abgeleitet wird, die den erforderlichen Code darin erstellen super_any_t::set_operation_to. Bei Mehrfachvererbung dürfte das sogar etwa so kurz sein wie die Zuweisung an Funktionszeiger. Da Sie die Eingabefunktionen auf zustandslos/rein beschränken, speichern Sie diese Zeiger einmal pro in einer vtable Liste der Eingangsfunktionstypen scheint möglich.

    – dyp

    9. August 16 um 7:33 Uhr

  • “da ich keinen C++17-Compiler habe.” Du meinst abgesehen von Wandbox und einem der anderen Online-Compiler? Auch: apt.llvm.org.

    – TemplateRex

    9. August 16 um 7:59 Uhr

  • @TemplateRex Ich denke, Yakk meinte, dass keiner der Compiler C ++ 17 vollständig unterstützt, hier automatische Vorlagenparameter …

    – WF

    9. August 16 um 8:52 Uhr

  • @dyp eine manuelle Vtable, in der wir einen Zeiger auf eine Statik erstellen tuple erstellt auf einer pro Typ gespeicherten Basis, wäre besser. Overhead von einem Zeiger pro Super-Any-Instanz, anstatt einer pro Methode pro Instanz. Dies vermeidet die Verwendung von new (entweder Platzierung oder nicht). Die Platzierung neu ist nicht gut, da es “Glück” erfordert, um die richtige Größe zu erhalten (oder statische Asserts oder nicht verrückte Compiler), und außerdem ein Maß an Indierktion hinzufügt, es sei denn, Sie sind sehr vorsichtig.

    – Yakk – Adam Nevraumont

    9. August 16 um 11:22 Uhr

  • Ist es möglich, die Signatur in loszuwerden? any_method_function ? Es könnte also möglich sein, Vorlagenparameter zu verwenden? Wie std::variant mit std::visit.

    – Turm120

    11. Juni 18 um 12:37 Uhr

1643906411 94 Typ loschen Typ loschen irgendwelche Fragen
cpplerner

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

  • Slick unter Verwendung der Signatur eines Funktionszeigers einschließlich des Typs der any_methodaber es beschränkt Sie grundsätzlich auf eindeutig typisierte any_methods (zumindest auf demselben Objekt). Ich sehe keine Möglichkeit zu migrieren auto* Zeigerargumente in der super_any<&print> leicht. Ich vermute einen Hack mit einem auto* Schild. Ich denke, das ist ein Eckfall, da wir den Kompilierzeit-Polymorphismus in der benötigen any_methodund auto&& self in einem Lambda ist der einfachste Weg, dies zu tun. Viel kleiner als meiner! (+1)

    – Yakk – Adam Nevraumont

    10. August 16 um 16:01 Uhr


.

757610cookie-checkTyp löschen Typ löschen, “irgendwelche” Fragen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy