c++ - metodo - razones de rentabilidad interpretacion
¿Hay una manera de devolver una abstracción de una función sin usar la nueva(por razones de rendimiento) (9)
Por ejemplo, tengo alguna función pet_maker()
que crea y devuelve un Cat
o un Dog
como Pet
base. Quiero llamar a esta función muchas veces y hacer algo con la Pet
devuelta.
Tradicionalmente, lo haría con el Cat
o el Dog
en pet_maker()
y le devolvería un puntero, sin embargo, la new
llamada es mucho más lenta que hacer todo lo que está en la pila.
¿Hay alguna manera clara en la que alguien pueda pensar en regresar como una abstracción sin tener que hacer lo nuevo cada vez que se llama a la función, o hay alguna otra forma en la que pueda crear y devolver abstracciones rápidamente?
¿Hay alguna manera clara en la que alguien pueda pensar en regresar como una abstracción sin tener que hacer lo
new
cada vez que se llama a la función, o hay alguna otra forma en la que pueda crear y devolver abstracciones rápidamente?
TL; DR: No es necesario asignar la función si ya hay suficiente memoria para trabajar.
Una forma simple sería crear un puntero inteligente que sea ligeramente diferente de sus hermanos: contendría un búfer en el que almacenaría el objeto. ¡Incluso podemos hacerlo anulable!
Versión larga:
Presentaré el borrador en orden inverso, desde la motivación hasta los detalles difíciles:
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();
}
El resultado esperado:
Miaou Woof Miaou
que puedes encontrar aquí .
Tenga en cuenta que es necesario especificar el tamaño máximo y la alineación de los valores derivados que pueden admitirse. No hay forma de evitar eso.
Por supuesto, verificamos estáticamente si la persona que llama intentaría construir un valor con un tipo inapropiado para evitar cualquier desagrado.
La principal desventaja, por supuesto, es que debe ser al menos tan grande (y alineada) como su variante más grande, y todo esto debe predecirse con anticipación. Por lo tanto, esto no es una bala de plata, pero en cuanto al rendimiento, la ausencia de asignación de memoria puede sacudir.
¿Como funciona? Usando esta clase de alto nivel (y el ayudante):
// 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
Esencialmente, esto es como cualquier contenedor de STL. La mayor parte de la complejidad está en la redefinición de la construcción, movimiento, copia y destrucción. Por lo demás es bastante simple.
Hay dos puntos a destacar:
Utilizo un enfoque basado en etiquetas para manejar capacidades:
- por ejemplo, un constructor de copia solo está disponible si se pasa
CopyConstructibleTag
- Si se pasa el
CopyConstructibleTag
, todos los tipos pasados abuild
deben ser construibles por copia
- por ejemplo, un constructor de copia solo está disponible si se pasa
Algunas operaciones se proporcionan incluso si los objetos no tienen la capacidad, siempre que exista alguna forma alternativa de proporcionarlos.
Obviamente, todos los métodos conservan el invariante de que el valor polymorphic_value
nunca está vacío.
También hay un detalle complicado relacionado con las asignaciones: la asignación solo está bien definida si ambos objetos son del mismo tipo dinámico, lo que verificamos con las m_vtable == other.m_vtable
controles.
Para completar, las piezas faltantes usadas para potenciar esta clase:
//
// 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
El único truco que utilizo aquí es permitir al usuario configurar si los tipos que utilizará en este contenedor se pueden construir, mover asignados, ... a través de etiquetas de capacidad. Una serie de operaciones están codificadas en estas etiquetas y se desactivarán o serán menos eficientes si la capacidad solicitada
Por ejemplo, tengo alguna función
pet_maker()
que crea y devuelve unCat
o unDog
comoPet
base. Quiero llamar a esta función muchas veces y hacer algo con laPet
devuelta.
Si va a desechar a la mascota inmediatamente después de haber hecho algo con ella, puede usar la técnica que se muestra en el siguiente ejemplo:
#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);
}
No se requiere asignación en absoluto. La idea básica es revertir la lógica: la fábrica ya no devuelve un objeto. En su lugar, llama a una función que proporciona la instancia correcta como referencia.
El problema con este enfoque surge si desea copiar y almacenar el objeto en algún lugar.
Como no queda claro en la pregunta, vale la pena proponer también esta solución.
Cada asignación es una sobrecarga, por lo que puede obtener beneficios al asignar matrices completas de objetos en lugar de un objeto a la vez.
Podrías usar std::deque para lograr esto:
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'';
}
La razón para usar std::deque es que nunca reasigna sus elementos cuando agrega otros nuevos, por lo que los punteros que distribuye siempre serán válidos hasta que se PetMaker
propio PetMaker
.
Un beneficio adicional a esto sobre la asignación de objetos individualmente es que no necesitan ser eliminados o colocados en un puntero inteligente , el std::deque administra su vida útil.
Depende del caso de uso exacto que tenga y de las restricciones que esté dispuesto a tolerar. Por ejemplo, si está de acuerdo con reutilizar los mismos objetos en lugar de tener copias nuevas cada vez, podría devolver referencias a objetos estáticos dentro de la función:
Pet& pet_maker()
{
static Dog dog;
static Cat cat;
//...
if(shouldReturnDog) {
//manipulate dog as necessary
//...
return dog;
}
else
{
//manipulate cat as necessary
//...
return cat;
}
}
Esto funciona si el código del cliente acepta que no es el propietario del objeto devuelto y que se reutilizan las mismas instancias físicas.
Hay otros trucos posibles si este conjunto particular de suposiciones no es adecuado.
En algún momento alguien tendrá que asignar la memoria e inicializar los objetos. Si realizarlos a pedido, usar el montón a través de new
es demasiado largo, entonces, ¿por qué no realizar una asignación previa de varios en un grupo? Luego, puede inicializar cada objeto individual según sea necesario. El inconveniente es que puedes tener un montón de objetos extra por un tiempo.
Si el problema, y no la asignación de memoria, es en realidad la inicialización del objeto, entonces puede considerar mantener un objeto Pototype alrededor y usar el patrón Pototype para una inicialización más rápida.
Para obtener los mejores resultados, la asignación de memoria es un problema y el tiempo de inicialización, puede combinar ambas estrategias.
Es posible que desee considerar el uso de una variante (Boost). La persona que llama necesitará un paso adicional, pero podría satisfacer sus necesidades:
#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();
}
Salida:
Cat Dog
Puede crear una instancia de asignador de pila (con algún límite máximo, por supuesto) y pasar eso como un argumento a su función pet_maker
. Luego, en lugar de una new
normal, realice una placement new
en la dirección proporcionada por el asignador de pila.
Probablemente también puede predeterminar como new
al exceder max_size
del asignador de pila.
Una forma es calcular, de antemano mediante el análisis, la cantidad de cada tipo de objeto que necesita su programa.
Luego, puede asignar matrices de un tamaño apropiado por adelantado, siempre y cuando tenga contabilidad para realizar un seguimiento de la asignación.
Por ejemplo;
#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
}
}
Por supuesto, la gran compensación es el requisito de determinar explícitamente el número de cada tipo de animal por adelantado, y rastrear por separado cada tipo.
Sin embargo, si desea evitar la asignación de memoria dinámica (operador new
), de esta manera, por muy draconiana que parezca, ofrece una garantía absoluta. El uso del operador new
explícitamente permite determinar el número de objetos necesarios en el tiempo de ejecución. A la inversa, para evitar el uso del operador new
pero permitir que alguna función acceda de manera segura a una serie de objetos, es necesario predeterminar la cantidad de objetos.
Usar nuevo es bastante inevitable si quieres polimorfismo. Pero la razón por la que el nuevo funciona lentamente es porque busca memoria libre en todo momento. Lo que podría hacer es escribir su propio operador nuevo, lo que podría, en teoría, utilizar fragmentos de memoria asignados previamente y ser muy rápido.
Este artículo cubre muchos aspectos de lo que podría necesitar.