it-swarm.com.de

Gibt es eine Möglichkeit, eine Abstraktion von einer Funktion zurückzugeben, ohne eine neue zu verwenden (aus Leistungsgründen)?

Zum Beispiel habe ich eine Funktion pet_maker(), die einen Catoder einen Dogals Basis Peterstellt und zurückgibt. Ich möchte diese Funktion viele Male aufrufen und etwas mit dem Pettun, der zurückgegeben wird.

Herkömmlicherweise würde ich newden Catoder Dogin pet_maker() verwenden und einen Zeiger darauf zurückgeben, jedoch ist der Aufruf von newviel langsamer, als alles auf dem Stack auszuführen.

Gibt es eine gute Möglichkeit, die man sich vorstellen kann, als Abstraktion zurückzukehren, ohne bei jedem Aufruf der Funktion eine neue Funktion ausführen zu müssen, oder gibt es eine andere Möglichkeit, wie ich Abstraktionen schnell erstellen und zurückgeben kann?

23
sji

Die Verwendung von new ist ziemlich unvermeidlich, wenn Sie Polymorphismus wollen. Aber der Grund, warum neu langsam arbeitet, ist, dass jedes Mal nach freiem Speicher gesucht wird. Was Sie tun könnten, ist, Ihren eigenen Operator neu zu schreiben, der theoretisch beispielsweise vorab zugewiesene Speicherabschnitte verwenden und sehr schnell sein könnte.

Dieser Artikel deckt viele Aspekte dessen ab, was Sie möglicherweise benötigen.

20
Armen Tsirunyan

Jede Zuordnung ist ein Aufwand, sodass Sie möglicherweise Vorteile erzielen, wenn Sie ganze Arrays von Objekten anstatt jeweils eines Objekts zuordnen.

Sie könnten std :: deque verwenden, um dies zu erreichen:

class Pet { public: virtual ~Pet() {} virtual std::string talk() const = 0; };
class Cat: public Pet { std::string talk() const override { return "meow"; }};
class Dog: public Pet { std::string talk() const override { return "woof"; }};
class Pig: public Pet { std::string talk() const override { return "oink"; }};

class PetMaker
{
    // std::deque never re-allocates when adding
    // elements which is important when distributing
    // pointers to the elements
    std::deque<Cat> cats;
    std::deque<Dog> dogs;
    std::deque<Pig> pigs;

public:

    Pet* make()
    {
        switch(std::Rand() % 3)
        {
            case 0:
                cats.emplace_back();
                return &cats.back();
            case 1:
                dogs.emplace_back();
                return &dogs.back();
        }
        pigs.emplace_back();
        return &pigs.back();
    }
};

int main()
{
    std::srand(std::time(0));

    PetMaker maker;

    std::vector<Pet*> pets;

    for(auto i = 0; i < 100; ++i)
        pets.Push_back(maker.make());

    for(auto pet: pets)
        std::cout << pet->talk() << '\n';
}

Der Grund für die Verwendung von std :: deque ist, dass es seine Elemente niemals neu zuordnet wenn Sie neue hinzufügen, sodass die von Ihnen verteilten Zeiger immer gültig bleiben, bis die PetMaker selbst gelöscht wird.

Ein zusätzlicher Vorteil gegenüber der individuellen Zuweisung von Objekten besteht darin, dass sie nicht gelöscht oder in einem Smart Pointer abgelegt werden müssen, der std :: deque verwaltet ihre Lebensdauer.

10
Galik

Gibt es eine gute Möglichkeit, die man sich vorstellen kann, als Abstraktion zurückzukehren, ohne dass bei jedem Aufruf der Funktion die Variable neweingegeben werden muss, oder gibt es eine andere Möglichkeit, wie ich Abstraktionen schnell erstellen und zurückgeben kann?

TL; DR: Die Funktion muss nicht reservieren, wenn bereits genügend Speicher zum Arbeiten vorhanden ist.

Ein einfacher Weg wäre, einen intelligenten Zeiger zu erstellen, der sich geringfügig von seinen Geschwistern unterscheidet: Er würde einen Puffer enthalten, in dem er das Objekt speichern würde. Wir können es sogar nicht nullbar machen!


Lange Version:

Ich präsentiere den Rohentwurf in umgekehrter Reihenfolge, von der Motivation bis zu den kniffligen Details:

class Pet {
public:
    virtual ~Pet() {}

    virtual void say() = 0;
};

class Cat: public Pet {
public:
    virtual void say() override { std::cout << "Miaou\n"; }
};

class Dog: public Pet {
public:
    virtual void say() override { std::cout << "Woof\n"; }
};

template <>
struct polymorphic_value_memory<Pet> {
    static size_t const capacity = sizeof(Dog);
    static size_t const alignment = alignof(Dog);
};

typedef polymorphic_value<Pet> any_pet;

any_pet pet_factory(std::string const& name) {
    if (name == "Cat") { return any_pet::build<Cat>(); }
    if (name == "Dog") { return any_pet::build<Dog>(); }

    throw std::runtime_error("Unknown pet name");
}

int main() {
    any_pet pet = pet_factory("Cat");
    pet->say();
    pet = pet_factory("Dog");
    pet->say();
    pet = pet_factory("Cat");
    pet->say();
}

Die erwartete Ausgabe:

Miaou
Woof
Miaou

welche findest du hier .

Beachten Sie, dass die maximale Größe und Ausrichtung der unterstützten abgeleiteten Werte angegeben werden muss. Daran führt kein Weg vorbei.

Natürlich prüfen wir statisch, ob der Aufrufer versuchen würde, einen Wert mit einem ungeeigneten Typ zu erstellen, um Unannehmlichkeiten zu vermeiden.

Der Hauptnachteil ist natürlich, dass es mindestens so groß (und ausgerichtet) sein muss wie seine größte Variante, und all dies muss vorhergesagt werden. Dies ist also keine Wunderwaffe, aber in Bezug auf die Leistung kann das Fehlen einer Speicherzuweisung den Ausschlag geben.


Wie funktioniert es? Mit dieser übergeordneten Klasse (und dem Helfer):

//  To be specialized for each base class:
//  - provide capacity member (size_t)
//  - provide alignment member (size_t)
template <typename> struct polymorphic_value_memory;

template <typename T,
          typename CA = CopyAssignableTag,
          typename CC = CopyConstructibleTag,
          typename MA = MoveAssignableTag,
          typename MC = MoveConstructibleTag>
class polymorphic_value {
    static size_t const capacity = polymorphic_value_memory<T>::capacity;
    static size_t const alignment = polymorphic_value_memory<T>::alignment;

    static bool const move_constructible = std::is_same<MC, MoveConstructibleTag>::value;
    static bool const move_assignable = std::is_same<MA, MoveAssignableTag>::value;
    static bool const copy_constructible = std::is_same<CC, CopyConstructibleTag>::value;
    static bool const copy_assignable = std::is_same<CA, CopyAssignableTag>::value;

    typedef typename std::aligned_storage<capacity, alignment>::type storage_type;

public:
    template <typename U, typename... Args>
    static polymorphic_value build(Args&&... args) {
        static_assert(
            sizeof(U) <= capacity,
            "Cannot Host such a large type."
        );

        static_assert(
            alignof(U) <= alignment,
            "Cannot Host such a largely aligned type."
        );

        polymorphic_value result{NoneTag{}};
        result.m_vtable = &build_vtable<T, U, MC, CC, MA, CA>();
        new (result.get_ptr()) U(std::forward<Args>(args)...);
        return result;
    }

    polymorphic_value(polymorphic_value&& other): m_vtable(other.m_vtable), m_storage() {
        static_assert(
            move_constructible,
            "Cannot move construct this value."
        );

        (*m_vtable->move_construct)(&other.m_storage, &m_storage);

        m_vtable = other.m_vtable;
    }

    polymorphic_value& operator=(polymorphic_value&& other) {
        static_assert(
            move_assignable || move_constructible,
            "Cannot move assign this value."
        );

        if (move_assignable && m_vtable == other.m_vtable)
        {
            (*m_vtable->move_assign)(&other.m_storage, &m_storage);
        }
        else
        {
            (*m_vtable->destroy)(&m_storage);

            m_vtable = other.m_vtable;
            (*m_vtable->move_construct)(&other.m_storage, &m_storage);
        }

        return *this;
    }

    polymorphic_value(polymorphic_value const& other): m_vtable(other.m_vtable), m_storage() {
        static_assert(
            copy_constructible,
            "Cannot copy construct this value."
        );

        (*m_vtable->copy_construct)(&other.m_storage, &m_storage);
    }

    polymorphic_value& operator=(polymorphic_value const& other) {
        static_assert(
            copy_assignable || (copy_constructible && move_constructible),
            "Cannot copy assign this value."
        );

        if (copy_assignable && m_vtable == other.m_vtable)
        {
            (*m_vtable->copy_assign)(&other.m_storage, &m_storage);
            return *this;
        }

        //  Exception safety
        storage_type tmp;
        (*other.m_vtable->copy_construct)(&other.m_storage, &tmp);

        if (move_assignable && m_vtable == other.m_vtable)
        {
            (*m_vtable->move_assign)(&tmp, &m_storage);
        }
        else
        {
            (*m_vtable->destroy)(&m_storage);

            m_vtable = other.m_vtable;
            (*m_vtable->move_construct)(&tmp, &m_storage);
        }

        return *this;
    }

    ~polymorphic_value() { (*m_vtable->destroy)(&m_storage); }

    T& get() { return *this->get_ptr(); }
    T const& get() const { return *this->get_ptr(); }

    T* operator->() { return this->get_ptr(); }
    T const* operator->() const { return this->get_ptr(); }

    T& operator*() { return this->get(); }
    T const& operator*() const { return this->get(); }

private:
    polymorphic_value(NoneTag): m_vtable(0), m_storage() {}

    T* get_ptr() { return reinterpret_cast<T*>(&m_storage); }
    T const* get_ptr() const { return reinterpret_cast<T const*>(&m_storage); }

    polymorphic_value_vtable const* m_vtable;
    storage_type m_storage;
}; // class polymorphic_value

Im Grunde ist dies genau wie bei jedem STL-Container. Der größte Teil der Komplexität besteht in der Neudefinition von Konstruktion, Bewegung, Kopie und Zerstörung. Sonst ist es ganz einfach.

Es gibt zwei wichtige Punkte:

  1. Ich verwende einen tagbasierten Ansatz für die Handhabung von Funktionen:

    • beispielsweise ist ein Kopierkonstruktor nur verfügbar, wenn der Name der CopyConstructibleTag übergeben wird
    • wenn CopyConstructibleTagübergeben wird, müssen alle an buildmuss übergebenen Typen kopierkonstruierbar sein
  2. Einige Operationen werden bereitgestellt, auch wenn die Objekte nicht über die entsprechenden Funktionen verfügen, sofern eine alternative Methode zu ihrer Bereitstellung vorhanden ist

Offensichtlich behalten alle Methoden die Invariante bei, dass der polymorphic_value niemals leer ist.

Es gibt auch ein heikles Detail in Bezug auf Zuweisungen: Zuweisungen sind nur dann genau definiert, wenn beide Objekte vom selben dynamischen Typ sind, den wir mit den m_vtable == other.m_vtable-Prüfungen überprüfen.


Der Vollständigkeit halber die fehlenden Teile, die zum Hochfahren dieser Klasse verwendet wurden:

//
//  VTable, with nullable methods for run-time detection of capabilities
//
struct NoneTag {};
struct MoveConstructibleTag {};
struct CopyConstructibleTag {};
struct MoveAssignableTag {};
struct CopyAssignableTag {};

struct polymorphic_value_vtable {
    typedef void (*move_construct_type)(void* src, void* dst);
    typedef void (*copy_construct_type)(void const* src, void* dst);
    typedef void (*move_assign_type)(void* src, void* dst);
    typedef void (*copy_assign_type)(void const* src, void* dst);
    typedef void (*destroy_type)(void* dst);

    move_construct_type move_construct;
    copy_construct_type copy_construct;
    move_assign_type move_assign;
    copy_assign_type copy_assign;
    destroy_type destroy;
};


template <typename Base, typename Derived>
void core_move_construct_function(void* src, void* dst) {
    Derived* derived = reinterpret_cast<Derived*>(src);
    new (reinterpret_cast<Base*>(dst)) Derived(std::move(*derived));
} // core_move_construct_function

template <typename Base, typename Derived>
void core_copy_construct_function(void const* src, void* dst) {
    Derived const* derived = reinterpret_cast<Derived const*>(src);
    new (reinterpret_cast<Base*>(dst)) Derived(*derived);
} // core_copy_construct_function

template <typename Derived>
void core_move_assign_function(void* src, void* dst) {
    Derived* source = reinterpret_cast<Derived*>(src);
    Derived* destination = reinterpret_cast<Derived*>(dst);
    *destination = std::move(*source);
} // core_move_assign_function

template <typename Derived>
void core_copy_assign_function(void const* src, void* dst) {
    Derived const* source = reinterpret_cast<Derived const*>(src);
    Derived* destination = reinterpret_cast<Derived*>(dst);
    *destination = *source;
} // core_copy_assign_function

template <typename Derived>
void core_destroy_function(void* dst) {
    Derived* d = reinterpret_cast<Derived*>(dst);
    d->~Derived();
} // core_destroy_function


template <typename Tag, typename Base, typename Derived>
typename std::enable_if<
    std::is_same<Tag, MoveConstructibleTag>::value,
    polymorphic_value_vtable::move_construct_type
>::type 
build_move_construct_function()
{
    return &core_move_construct_function<Base, Derived>;
} // build_move_construct_function

template <typename Tag, typename Base, typename Derived>
typename std::enable_if<
    std::is_same<Tag, CopyConstructibleTag>::value,
    polymorphic_value_vtable::copy_construct_type
>::type 
build_copy_construct_function()
{
    return &core_copy_construct_function<Base, Derived>;
} // build_copy_construct_function

template <typename Tag, typename Derived>
typename std::enable_if<
    std::is_same<Tag, MoveAssignableTag>::value,
    polymorphic_value_vtable::move_assign_type
>::type 
build_move_assign_function()
{
    return &core_move_assign_function<Derived>;
} // build_move_assign_function

template <typename Tag, typename Derived>
typename std::enable_if<
    std::is_same<Tag, CopyAssignableTag>::value,
    polymorphic_value_vtable::copy_construct_type
>::type 
build_copy_assign_function()
{
    return &core_copy_assign_function<Derived>;
} // build_copy_assign_function


template <typename Base, typename Derived,
          typename MC, typename CC,
          typename MA, typename CA>
polymorphic_value_vtable const& build_vtable() {
    static polymorphic_value_vtable const V = {
        build_move_construct_function<MC, Base, Derived>(),
        build_copy_construct_function<CC, Base, Derived>(),
        build_move_assign_function<MA, Derived>(),
        build_copy_assign_function<CA, Derived>(),
        &core_destroy_function<Derived>
    };
    return V;
} // build_vtable

Der eine Trick, den ich hier benutze, besteht darin, den Benutzer konfigurieren zu lassen, ob die Typen, die er in diesem Container verwenden wird, über Fähigkeits-Tags erstellt, zugewiesen usw. verschoben werden können. Eine Reihe von Vorgängen sind für diese Tags codiert und werden entweder deaktiviert oder sind weniger effizient, wenn die angeforderte Fähigkeit vorliegt

10
Matthieu M.

Sie können eine Stack-Allokator-Instanz erstellen (natürlich mit einer gewissen Höchstgrenze) und diese als Argument an Ihre pet_maker -Funktion übergeben. Führen Sie dann anstelle der regulären new einen placement new für die vom Stapelzuweiser angegebene Adresse aus.

Sie können wahrscheinlich auch new vorgeben, wenn der max_size des Stack-Allokators überschritten wird.

6
Arunmu

Eine Möglichkeit besteht darin, im Voraus durch Analyse zu ermitteln, wie viele Objekte jedes Objekttyps von Ihrem Programm benötigt werden.

Anschließend können Sie Arrays mit einer geeigneten Größe im Voraus zuweisen, sofern Sie über eine Buchhaltung verfügen, um die Zuordnung zu verfolgen.

Zum Beispiel;

#include <array>

//   Ncats, Ndogs, etc are predefined constants specifying the number of cats and dogs

std::array<Cat, Ncats> cats;
std::array<Dog, Ndogs> dogs;

//  bookkeeping - track the returned number of cats and dogs

std::size_t Rcats = 0, Rdogs = 0;

Pet *pet_maker()
{
    // determine what needs to be returned

    if (return_cat)
    {
       assert(Rcats < Ncats);
       return &cats[Rcats++];
    }
    else if (return_dog)
    {
       assert(Rdogs < Ndogs);
       return &dogs[Rdogs++];
    }
    else
    {
        // handle other case somehow
    }
}

Der große Kompromiss besteht natürlich darin, die Anzahl der einzelnen Tiertypen im Voraus explizit zu bestimmen - und jeden Typ separat zu verfolgen.

Wenn Sie jedoch die dynamische Speicherzuweisung (Operator new) vermeiden möchten, ist dies - so drakonisch es auch scheinen mag - eine absolute Garantie. Durch die explizite Verwendung des Operators new kann die Anzahl der zur Laufzeit benötigten Objekte bestimmt werden. Um umgekehrt die Verwendung des Operators new zu vermeiden, aber einigen Funktionen den sicheren Zugriff auf eine Reihe von Objekten zu ermöglichen, muss die Anzahl der Objekte vorbestimmt werden.

4
Peter

Möglicherweise möchten Sie eine (Boost-) Variante in Betracht ziehen. Es erfordert einen zusätzlichen Schritt durch den Anrufer, aber es könnte Ihren Bedürfnissen entsprechen:

#include <boost/variant/variant.hpp>
#include <boost/variant/get.hpp>
#include <iostream>

using boost::variant;
using std::cout;


struct Pet {
    virtual void print_type() const = 0;
};

struct Cat : Pet {
    virtual void print_type() const { cout << "Cat\n"; }
};

struct Dog : Pet {
    virtual void print_type() const { cout << "Dog\n"; }
};


using PetVariant = variant<Cat,Dog>;
enum class PetType { cat, dog };


PetVariant make_pet(PetType type)
{
    switch (type) {
        case PetType::cat: return Cat();
        case PetType::dog: return Dog();
    }

    return {};
}

Pet& get_pet(PetVariant& pet_variant)
{
    return apply_visitor([](Pet& pet) -> Pet& { return pet; },pet_variant);
}




int main()
{
    PetVariant pet_variant_1 = make_pet(PetType::cat);
    PetVariant pet_variant_2 = make_pet(PetType::dog);
    Pet& pet1 = get_pet(pet_variant_1);
    Pet& pet2 = get_pet(pet_variant_2);
    pet1.print_type();
    pet2.print_type();
}

Ausgabe:

Katze Hund
3
Vaughn Cato

Irgendwann muss jemand den Speicher zuweisen und die Objekte initialisieren. Wenn es zu lange dauert, den Heap-Speicher über new bei Bedarf zu verwenden, warum dann nicht eine Anzahl von then in einem Pool vorab zuweisen? Anschließend können Sie jedes einzelne Objekt nach Bedarf initialisieren. Der Nachteil ist, dass möglicherweise ein paar zusätzliche Objekte für eine Weile herumliegen.

Wenn das eigentliche Initialisieren des Objekts das Problem ist und nicht die Speicherzuordnung, können Sie ein vorgefertigtes Objekt beibehalten und das Pototype - Muster zur schnelleren Initialisierung verwenden.

Um optimale Ergebnisse zu erzielen, ist die Speicherzuweisung ein Problem und die Initialisierungszeit können Sie beide Strategien kombinieren.

3
Ken Brittain

Dies hängt vom genauen Anwendungsfall ab und davon, welche Einschränkungen Sie tolerieren möchten. Wenn Sie beispielsweise dieselben Objekte nicht jedes Mal neu kopieren, sondern erneut verwenden möchten, können Sie Verweise auf statische Objekte in der Funktion zurückgeben:

Pet& pet_maker()
{
static Dog dog;
static Cat cat;

    //...

    if(shouldReturnDog) {
        //manipulate dog as necessary
        //...
        return dog;
    }
    else
    {
        //manipulate cat as necessary
        //...
        return cat;
    }
}

Dies funktioniert, wenn der Client-Code akzeptiert, dass er das zurückgegebene Objekt nicht besitzt und dieselben physischen Instanzen wiederverwendet werden.

Es sind andere Tricks möglich, wenn diese bestimmten Annahmen ungeeignet sind.

3
Smeeheey

Zum Beispiel habe ich eine Funktion pet_maker(), die einen Catoder einen Dogals Basis Peterstellt und zurückgibt. Ich möchte diese Funktion viele Male aufrufen und etwas mit dem Pettun, der zurückgegeben wird.

Wenn Sie das Haustier sofort entsorgen möchten, nachdem Sie etwas getan haben, können Sie die im folgenden Beispiel gezeigte Technik anwenden:

#include<iostream>
#include<utility>

struct Pet {
    virtual ~Pet() = default;
    virtual void foo() const = 0;
};

struct Cat: Pet {
    void foo() const override {
        std::cout << "cat" << std::endl;
    }
};

struct Dog: Pet {
    void foo() const override {
        std::cout << "dog" << std::endl;
    }
};

template<typename T, typename F>
void factory(F &&f) {
    std::forward<F>(f)(T{});
}

int main() {
    auto lambda = [](const Pet &pet) { pet.foo(); };
    factory<Cat>(lambda);
    factory<Dog>(lambda);
}

Überhaupt keine Zuordnung erforderlich. Die Grundidee ist, die Logik umzukehren: Die Fabrik gibt kein Objekt mehr zurück. Stattdessen wird eine Funktion aufgerufen, die die richtige Instanz als Referenz bereitstellt.
Das Problem bei diesem Ansatz tritt auf, wenn Sie das Objekt kopieren und irgendwo speichern möchten.
Da es aus der Frage nicht klar ist, lohnt es sich, auch diese Lösung vorzuschlagen.

1
skypjack