2017-02-16 3 views
9

C++ 14 Einführung generische lambdas (wenn das Auto-Schlüsselwort in den Lambda-Signaturen verwendet).C++ 17 Vektor von Generic (Polymorphic) lambdas

Gibt es eine Möglichkeit, sie in einem Vektor mit C++ 17 zu speichern?

Ich weiß, über diese bestehende Frage, aber es ist meine Bedürfnisse nicht passen: Can I have a std::vector of template function pointers?

Hier ist ein Beispielcode veranschaulicht, was ich tun möchte. (Bitte beachten Sie die Hinweise am Ende, bevor er antwortete)

#include <functional> 
#include <vector> 

struct A { 
    void doSomething() { 
     printf("A::doSomething()\n"); 
    } 
    void doSomethingElse() { 
     printf("A::doSomethingElse()\n"); 
    } 
}; 

struct B { 
    void doSomething() { 
     printf("B::doSomething()\n"); 
    } 
    void doSomethingElse() { 
     printf("B::doSomethingElse()\n"); 
    } 
}; 

struct TestRunner { 
    static void run(auto &actions) { 
     A a; 
     for (auto &action : actions) action(a); 
     B b; 
     for (auto &action : actions) action(b); // I would like to do it 
     // C c; ... 
    } 
}; 

void testCase1() { 
    std::vector<std::function<void(A&)>> actions; // Here should be something generic instead of A 
    actions.emplace_back([](auto &x) { 
     x.doSomething(); 
    }); 
    actions.emplace_back([](auto &x) { 
     x.doSomethingElse(); 
    }); 
    // actions.emplace_back(...) ... 
    TestRunner::run(actions); 
} 

void testCase2() { 
    std::vector<std::function<void(A&)>> actions; // Here should be something generic instead of A 
    actions.emplace_back([](auto &x) { 
     x.doSomething(); 
     x.doSomethingElse(); 
    }); 
    actions.emplace_back([](auto &x) { 
     x.doSomethingElse(); 
     x.doSomething(); 
    }); 
    // actions.emplace_back(...) ... 
    TestRunner::run(actions); 
} 

// ... more test cases : possibly thousands of them 
// => we cannot ennumerate them all (in order to use a variant type for the actions signatures for example) 

int main() { 
    testCase1(); 
    testCase2(); 

    return 0; 
} 

NOTES:

  • Der Code von A, B und TestRunner kann nicht geändert werden, wird nur der Code der Testfälle
  • I don Ich möchte nicht diskutieren, ob es gut oder falsch ist, Tests wie diese zu programmieren, dies ist off-topic (die Testterminologie wird hier nur verwendet, um zu verdeutlichen, dass ich nicht alle lambdas aufzählen kann). .))

Antwort

6

Es folgt eine mögliche Lösung (die ich nicht empfehlen würde, aber Sie haben ausdrücklich gesagt, dass Sie nicht diskutieren wollen, ob es gut oder falsch ist und so weiter).
Wie angefordert, A, B und TestRunner wurden nicht geändert (beiseite stellen die Tatsache, dass auto ist kein gültiger Funktionsparameter für TestRunner und ich setze es entsprechend).
Wenn Sie leicht ändern können TestRunner, kann die ganze Sache verbessert werden.
aber sagen, dass hier ist der Code:

#include <functional> 
#include <vector> 
#include <iostream> 
#include <utility> 
#include <memory> 
#include <type_traits> 

struct A { 
    void doSomething() { 
     std::cout << "A::doSomething()" << std::endl; 
    } 
    void doSomethingElse() { 
     std::cout << "A::doSomethingElse()" << std::endl; 
    } 
}; 

struct B { 
    void doSomething() { 
     std::cout << "B::doSomething()" << std::endl; 
    } 
    void doSomethingElse() { 
     std::cout << "B::doSomethingElse()" << std::endl; 
    } 
}; 

struct Base { 
    virtual void operator()(A &) = 0; 
    virtual void operator()(B &) = 0; 
}; 

template<typename L> 
struct Wrapper: Base, L { 
    Wrapper(L &&l): L{std::forward<L>(l)} {} 

    void operator()(A &a) { L::operator()(a); } 
    void operator()(B &b) { L::operator()(b); } 
}; 

struct TestRunner { 
    static void run(std::vector<std::reference_wrapper<Base>> &actions) { 
     A a; 
     for (auto &action : actions) action(a); 
     B b; 
     for (auto &action : actions) action(b); 
    } 
}; 

void testCase1() { 
    auto l1 = [](auto &x) { x.doSomething(); }; 
    auto l2 = [](auto &x) { x.doSomethingElse(); }; 

    auto w1 = Wrapper<decltype(l1)>{std::move(l1)}; 
    auto w2 = Wrapper<decltype(l2)>{std::move(l2)}; 

    std::vector<std::reference_wrapper<Base>> actions; 
    actions.push_back(std::ref(static_cast<Base &>(w1))); 
    actions.push_back(std::ref(static_cast<Base &>(w2))); 

    TestRunner::run(actions); 
} 

void testCase2() { 
    auto l1 = [](auto &x) { 
     x.doSomething(); 
     x.doSomethingElse(); 
    }; 

    auto l2 = [](auto &x) { 
     x.doSomethingElse(); 
     x.doSomething(); 
    }; 

    auto w1 = Wrapper<decltype(l1)>{std::move(l1)}; 
    auto w2 = Wrapper<decltype(l2)>{std::move(l2)}; 

    std::vector<std::reference_wrapper<Base>> actions; 
    actions.push_back(std::ref(static_cast<Base &>(w1))); 
    actions.push_back(std::ref(static_cast<Base &>(w2))); 

    TestRunner::run(actions); 
} 

int main() { 
    testCase1(); 
    testCase2(); 

    return 0; 
} 

ich nicht sehen, wie man kann inhomogene Lambdas in einem Vektor zu speichern, weil sie einfach nicht homogene Typen haben.
Wie auch immer, durch die Definition einer Schnittstelle (siehe Base) und die Verwendung einer Template-Klasse (siehe Wrapper), die von der angegebenen Schnittstelle und einem Lambda erbt, können wir die Anfragen an das angegebene generische Lambda weiterleiten und trotzdem eine homogene Schnittstelle haben.
Mit anderen Worten, der Schlüssel Teil der Lösung sind die folgenden Klassen:

struct Base { 
    virtual void operator()(A &) = 0; 
    virtual void operator()(B &) = 0; 
}; 

template<typename L> 
struct Wrapper: Base, L { 
    Wrapper(L &&l): L{std::forward<L>(l)} {} 

    void operator()(A &a) { L::operator()(a); } 
    void operator()(B &b) { L::operator()(b); } 
}; 

Wo ein Wrapper von einem Lambda erzeugt werden kann, wie es folgt:

auto l1 = [](auto &) { /* ... */ }; 
auto w1 = Wrapper<decltype(l1)>{std::move(l1)}; 

Leider für die Anforderung war Um nicht TestRunner zu ändern, musste ich std::ref und std::reference_wrapper verwenden, um Referenzen in den Vektor zu setzen.

Sehen Sie es auf wandbox.

+0

Auto ist ein gültiger Funktionsparameter für TestRunner in C++ 14 (https://ideone.com/4931Ht) – infiniteLoop

+1

Diese polymorphe Lösung ist sehr clever +1. –

+1

@infiniteLoop Sehen Sie, wie es auf [wandbox] (http://melpon.org/wandbox/permlink/qrrFmVRri4bxM0cy) mit 'auto' läuft. Beachten Sie, dass es eine GCC-Erweiterung ist, kein gültiger Parameter in C++ 14 für freie Funktionen. – skypjack

0

Was Sie wollen, ist eine Erweiterung von std::function.

std::function<Sig> ist eine auflöschbare Art, die diese bestimmte Signatur modellieren kann. Wir wollen all diese Funktionalität, aber mit mehr Signaturen, und alle diese Signaturen sind überladbar. Wo das schwierig wird, ist, dass wir einen linearen Stapel von Überladungen brauchen.Diese Antwort setzt die neue C++ 17-Regel voraus, die das Expandieren von Parameterpaketen in einer using-Deklaration zulässt, und baut stückweise von Grund auf auf. Auch diese Antwort konzentriert sich nicht darauf, alle Kopien/Filme wo nötig zu vermeiden, ich baue nur das Gerüst. Außerdem muss es mehr SFINAE geben.


Zuerst müssen wir einen virtuellen Call-Betreiber für eine bestimmte Signatur:

template <class Sig> 
struct virt_oper_base; 

template <class R, class... Args> 
struct virt_oper_base<R(Args...)> 
{ 
    virtual R call(Args...) = 0; 
}; 

Und etwas Gruppe diejenigen zusammen:

template <class... Sigs> 
struct base_placeholder : virt_oper_base<Sigs>... 
{ 
    virtual ~base_placeholder() = default; 
    using virt_oper_base<Sigs>::call...; // <3   
    virtual base_placeholder* clone() = 0; // for the copy constructor 
}; 

Jetzt das lästige Teil. Wir brauchen eine placeholder<F, Sigs...>, um jede dieser call() s zu überschreiben. Es kann einen besseren Weg, dies zu tun, aber der beste Weg, ich ist einfiel zwei Typliste Template-Parameter zu haben, und nur jede Signatur von einem zum anderen zu bewegen, wie wir mit ihnen fertig:

template <class... > 
struct typelist; 

template <class F, class Done, class Sigs> 
struct placeholder_impl; 

template <class F, class... Done, class R, class... Args, class... Sigs> 
struct placeholder_impl<F, typelist<Done...>, typelist<R(Args...), Sigs...>> 
    : placeholder_impl<F, typelist<Done..., R(Args...)>, typelist<Sigs...>> 
{ 
    using placeholder_impl<F, typelist<Done..., R(Args...)>, typelist<Sigs...>>::placeholder_impl; 

    R call(Args... args) override { 
     return this->f(args...); 
    }  
}; 

template <class F, class... Done> 
struct placeholder_impl<F, typelist<Done...>, typelist<>> 
    : base_placeholder<Done...> 
{ 
    placeholder_impl(F f) : f(std::move(f)) { } 
    F f; 
}; 

template <class F, class... Sigs> 
struct placeholder : 
    placeholder_impl<F, typelist<>, typelist<Sigs...>> 
{ 
    using placeholder_impl<F, typelist<>, typelist<Sigs...>>::placeholder_impl; 

    base_placeholder<Sigs...>* clone() override { 
     return new placeholder<F, Sigs...>(*this); 
    } 
}; 

Diese könnte mehr Sinn ergeben, wenn ich die Hierarchie zeichne. Lassen Sie uns sagen, dass wir Ihre zwei Unterschriften haben: void(A&) und void(B&):

virt_oper_base<void(A&)>  virt_oper_base<void(B&)> 
    virtual void(A&) = 0;   virtual void(B&) = 0; 
     ↑       ↑ 
     ↑       ↑ 
base_placeholder<void(A&), void(B&)> 
    virtual ~base_placeholder() = default; 
    virtual base_placeholder* clone() = 0; 
     ↑ 
placeholder_impl<F, typelist<void(A&), void(B&)>, typelist<>> 
    F f; 
     ↑ 
placeholder_impl<F, typelist<void(A&)>, typelist<void(B&)>> 
    void call(B&) override; 
     ↑ 
placeholder_impl<F, typelist<>, typelist<void(A&), void(B&)>> 
    void call(A&) override; 
     ↑ 
placeholder<F, void(A&), void(B&)> 
    base_placeholder<void(A&), void(B&)>* clone(); 

Wir brauchen einen Weg, um zu überprüfen, ob eine gegebene Funktion einer Signatur erfüllt:

template <class F, class Sig> 
struct is_sig_callable; 

template <class F, class R, class... Args> 
struct is_sig_callable<F, R(Args...)> 
    : std::is_convertible<std::result_of_t<F(Args...)>, R> 
{ }; 

Und jetzt haben wir nur all das verwenden. Wir haben unsere Top-Level function Klasse, die eine base_placeholder Mitglied haben wird, deren Lebensdauer es verwaltet.

template <class... Sigs> 
class function 
{ 
    base_placeholder<Sigs...>* holder_; 
public: 
    template <class F, 
     std::enable_if_t<(is_sig_callable<F&, Sigs>::value && ...), int> = 0> 
    function(F&& f) 
     : holder_(new placeholder<std::decay_t<F>, Sigs...>(std::forward<F>(f))) 
    { } 

    ~function() 
    { 
     delete holder_; 
    } 

    function(function const& rhs) 
     : holder_(rhs.holder_->clone()) 
    { } 

    function(function&& rhs) noexcept 
     : holder_(rhs.holder_) 
    { 
     rhs.holder_ = nullptr; 
    } 

    function& operator=(function rhs) noexcept 
    { 
     std::swap(holder_, rhs.holder_); 
     return *this; 
    } 

    template <class... Us> 
    auto operator()(Us&&... us) 
     -> decltype(holder_->call(std::forward<Us>(us)...)) 
    { 
     return holder_->call(std::forward<Us>(us)...); 
    }  
}; 

Und jetzt haben wir eine Multi-Signatur, Typ gelöscht, Funktionsobjekt mit Wert Semantik. Was Sie dann wollen, ist nur:

std::vector<function<void(A&), void(B&)>> actions; 
0

Es ist nicht möglich, Funktionsvorlagen in irgendeiner Weise, Form oder Form zu speichern. Sie sind keine Daten. (Funktionen sind auch keine Daten, aber Funktionszeiger sind). Beachten Sie, dass es std :: function, aber keine std :: function_template gibt. Es gibt virtuelle Funktionen, aber keine virtuellen Funktionsvorlagen. Es gibt Funktionszeiger, aber keine Funktionsschablonenzeiger. Dies sind alles Manifestationen einer einfachen Tatsache: Zur Laufzeit gibt es keine Vorlagen.

Ein generisches Lambda ist nur ein Objekt mit einer operator() Memberfunktionsvorlage. Alles oben genannte gilt auch für Mitgliedervorlagen.

Sie können einen endlichen, kompilierzeitbestimmten Satz von Vorlagenspezialisierungen erhalten, um sich wie ein Objekt zu verhalten, aber das ist kein Unterschied zu einem Objekt, das nur eine endliche Menge virtueller Funktionen oder Funktionszeiger hat. In Ihrer Situation ist es ein Äquivalent eines

von mit
std::vector < 
    std::tuple < 
     std::function<void(A&)>, 
     std::function<void(B&)> 
    > 
> 

Es sollte möglich sein, eine generische Lambda zu einem solchen Paar mit einem benutzerdefinierten Konvertierungsfunktion zu konvertieren oder sogar ot wickeln in einem Objekt, das einen Operator() Mitglied hat Vorlage, also von außen würde es aussehen, als ob es genau das tut, was Sie wollen --- aber es wird nur mit den Typen A und B und sonst nichts funktionieren. Um einen anderen Typ hinzuzufügen, müssten Sie dem Tupel ein weiteres Element hinzufügen.