Подтвердить что ты не робот

Есть ли способ вернуть абстракцию из функции без использования новых (по соображениям производительности)

Например, у меня есть некоторая функция pet_maker(), которая создает и возвращает Cat или Dog в качестве базы Pet. Я хочу многократно вызывать эту функцию и делать что-то с возвращенным Pet.

Традиционно я бы new Cat или Dog в pet_maker() и возвращал указатель на него, однако вызов new намного медленнее, чем выполнение всего в стеке.

Есть ли опрятный способ, который любой может подумать о возврате как абстракции, без необходимости делать новое при каждом вызове функции, или есть какой-то другой способ, который я могу быстро создать и вернуть абстракции?

4b9b3361

Ответ 1

Использование нового в значительной степени неизбежно, если вы хотите полиморфизм. Но причина, по которой все работает медленно, заключается в том, что она каждый раз ищет свободную память. Что вы можете сделать, так это написать свой собственный оператор new, который теоретически мог бы, например, использовать предварительно выделенные фрагменты памяти и быть очень быстрым.

Эта статья охватывает многие аспекты того, что вам может понадобиться.

Ответ 2

Каждое распределение - это накладные расходы, поэтому вы можете получить преимущества, выделяя целые массивы объектов, а не один объект за раз.

Вы можете использовать std:: deque для достижения этой цели:

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

Причиной использования std:: deque является то, что он никогда не перераспределяет свои элементы при добавлении новых, поэтому указатели, которые вы распространяете, всегда остаются в силе до тех пор, пока PetMaker удаляется.

Дополнительным преимуществом для этого над распределением объектов по отдельности является то, что их не нужно удалять или помещать в интеллектуальный указатель, std:: deque управляет их срок службы.

Ответ 3

Есть ли опрятный способ, который любой может думать о возврате в качестве абстракции, не выполняя new каждый раз при вызове функции, или есть какой-то другой способ, который я могу быстро создать и вернуть абстракции?

TL; DR:. Функция не должна выделяться, если с ней уже имеется достаточная память.

Простым способом было бы создать умный указатель, который немного отличается от своих братьев и сестер: он будет содержать буфер, в котором он будет хранить объект. Мы даже можем сделать это не-nullable!


Длинная версия:

Я покажу черновик в обратном порядке, от мотивации до сложных деталей:

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

Ожидаемый результат:

Miaou
Woof
Miaou

который вы можете найти здесь.

Обратите внимание, что требуется указать максимальный размер и выравнивание полученных значений, которые могут поддерживаться. Ничего подобного.

Конечно, мы статически проверяем, будет ли вызывающий объект пытаться построить значение с неприемлемым типом, чтобы избежать неприятностей.

Основным недостатком, конечно же, является то, что он должен быть по крайней мере столь же большим (и выровненным), как его самый большой вариант, и все это должно быть предсказано заранее. Это, таким образом, не серебряная пуля, но по производительности отсутствие распределения памяти может качаться.


Как это работает? Используя этот класс высокого уровня (и помощник):

//  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

По существу, это похоже на любой контейнер STL. Основная часть сложности заключается в пересмотре конструкции, перемещении, копировании и уничтожении. Это в противном случае довольно просто.

Есть две точки примечания:

  • Я использую подход, основанный на тегах, к возможностям обработки:

    • Например, конструктор копирования доступен, только если CopyConstructibleTag передан
    • если передано CopyConstructibleTag, все типы, переданные в build, должны быть скопированы конструктивно
  • Некоторые операции предоставляются, даже если объекты не имеют возможности, если существует альтернативный способ их предоставления.

Очевидно, что все методы сохраняют инвариант, что polymorphic_value никогда не пуст.

Также есть сложная деталь, связанная с присваиваниями: назначение только четко определено, если оба объекта имеют один и тот же динамический тип, который мы проверяем с помощью проверок m_vtable == other.m_vtable.


Для полноты недостающих частей, используемых для включения этого класса:

//
//  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

Один трюк, который я использую здесь, - это позволить пользователю настроить, будут ли типы, которые он будет использовать в этом контейнере, могут быть перемещены, перемещены назначены,... через теги возможностей. Для этих тегов задействовано несколько операций и будет либо отключено, либо менее эффективно, если запрошенная возможность

Ответ 4

Вы можете создать экземпляр распределителя стека (с некоторым максимальным пределом конечно) и передать это как аргумент вашей функции pet_maker. Затем вместо обычного new выполните a placement new по адресу, предоставленному распределителем стека.

Возможно, вы также можете по умолчанию использовать new при превышении max_size распределителя стека.

Ответ 5

Один из способов заключается в том, чтобы заранее проанализировать, сколько из каждого типа объектов требуется вашей программе.

Затем вы можете заранее распределить массивы соответствующего размера, если у вас есть бухгалтерский учет для отслеживания распределения.

Например:

#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
    }
}

Конечно, большой компромисс в том, что требуется явно определить количество каждого типа животных заранее - и отдельно отслеживать каждый тип.

Однако, если вы хотите избежать динамического распределения памяти (оператор new), то этот способ - как драконовский, как может показаться - обеспечивает абсолютную гарантию. Использование оператора new явно позволяет определить количество объектов во время выполнения. И наоборот, чтобы избежать использования оператора new, но разрешить некоторым функциям безопасный доступ к нескольким объектам, необходимо предопределить количество объектов.

Ответ 6

Это зависит от конкретного случая использования, и какие ограничения вы готовы терпеть. Например, если вы в порядке с повторным использованием одних и тех же объектов, а не с новыми копиями каждый раз, вы можете вернуть ссылки на статические объекты внутри функции:

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

    //...

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

Это работает, если клиентский код согласен с тем, что он не владеет возвращенным объектом и что одни и те же физические экземпляры повторно используются.

Возможны другие трюки, если этот конкретный набор предположений непригоден.

Ответ 7

В какой-то момент кому-то придется выделять память и инициализировать объекты. Если вы делаете их по требованию, использование кучи через new занимает слишком много времени, то почему бы не предварительно выделить несколько из них в пуле. Затем вы можете инициализировать каждый отдельный объект по мере необходимости. Недостатком является то, что на некоторое время у вас может быть множество дополнительных объектов.

Если на самом деле инициализация объекта является проблемой, а не распределением памяти, то вы можете рассмотреть возможность хранения предварительно построенного объекта и использования Pototype для более быстрой инициализации.

Для достижения наилучших результатов распределение памяти - это проблема и время инициализации, вы можете объединить обе стратегии.

Ответ 8

Возможно, вам захочется рассмотреть вариант (Boost). Это потребует дополнительный шаг от вызывающего, но он может удовлетворить ваши потребности:

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

Вывод:

Cat
Dog

Ответ 9

Например, у меня есть некоторая функция pet_maker(), которая создает и возвращает Cat или Dog в качестве базы Pet. Я хочу многократно вызывать эту функцию и делать что-то с возвращенным Pet.

Если вы собираетесь отказаться от домашнего животного сразу после того, как вы что-то сделали с ним, вы можете использовать технику, показанную в следующем примере:

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

Никакое распределение не требуется вообще. Основная идея - вернуть логику: factory больше не возвращает объект. Вместо этого он вызывает функцию, предоставляющую правильный экземпляр в качестве ссылки.
Проблема с этим подходом возникает, если вы хотите скопировать и сохранить объект где-то.
Ибо не ясно из этого вопроса, стоит также предложить это решение.