Есть ли способ вернуть абстракцию из функции без использования новых (по соображениям производительности)
Например, у меня есть некоторая функция pet_maker()
, которая создает и возвращает Cat
или Dog
в качестве базы Pet
. Я хочу многократно вызывать эту функцию и делать что-то с возвращенным Pet
.
Традиционно я бы new
Cat
или Dog
в pet_maker()
и возвращал указатель на него, однако вызов new
намного медленнее, чем выполнение всего в стеке.
Есть ли опрятный способ, который любой может подумать о возврате как абстракции, без необходимости делать новое при каждом вызове функции, или есть какой-то другой способ, который я могу быстро создать и вернуть абстракции?
Ответы
Ответ 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 больше не возвращает объект. Вместо этого он вызывает функцию, предоставляющую правильный экземпляр в качестве ссылки.
Проблема с этим подходом возникает, если вы хотите скопировать и сохранить объект где-то.
Ибо не ясно из этого вопроса, стоит также предложить это решение.