Image by svgrepo.com
El tradicional basado en herencia y las funciones virtuales.
class IFoo {
public:
virtual void bar() const = 0;
virtual ~IFoo() = default;
};
class Foo1 final : public IFoo {
public:
void bar() const override {
std::cout << "I'm a Foo1 instance" << std::endl;
}
};
class Foo2 final : public IFoo {
public:
void bar() const override {
std::cout << "I'm a Foo2 instance" << std::endl;
}
};
std::vector<std::unique_ptr<IFoo>> foo_ptrs;
foo_ptrs.emplace_back(std::make_unique<Foo1>(Foo1()));
foo_ptrs.emplace_back(std::make_unique<Foo2>(Foo2()));
for (auto const& foo : foo_ptrs)
foo->bar();
I'm a Foo1 instance I'm a Foo2 instance
¿Utilizar una clase que no pertenece a la jerarquía?
class Foo3 {
public:
void bar() const { std::cout << "I'm a Foo3 instance" << std::endl; }
};
Es decir, las plantillas.
template<typename... Args>
void call_bar(Args&&... foos) { (foos.bar(), ...); }
call_bar(Foo1(), Foo2(), Foo3());
I'm a Foo1 instance I'm a Foo2 instance I'm a Foo3 instance
¿Almacenar distantas instancias de clases de plantilla en un contenedor...
... dinámico en tiempo de ejecución...
... para cualquier tipo que cumpla la interfaz?
...la herencia permite utilizar tipos desconocidos a través de las interfaces.
...las plantillas permiten generar código de forma automática.
class FooConcept {
public:
virtual void bar() const = 0;
virtual ~FooConcept() = default;
};
Utilizando la herencia proporcionando una clase base para las plantillas, se consigue que estas no tengan por qué ser conocidas entre distintas unidades de traducción.
template<typename T>
class FooModel final : public FooConcept {
T self_;
public:
template<typename U>
FooModel(U&& self) : self_{ std::forward<U>(self) } {}
void bar() const override { self_.bar(); }
};
Utilizando las plantillas, se consigue liberar al usuario de tener que escribir código para adaptarse a la interfaz.
class Foo {
std::unique_ptr<FooConcept> self_;
public:
template<typename T>
Foo(T&& self)
: self_{ std::make_unique<FooModel<T>>(std::forward<T>(self)) }
{}
void bar() const { self_->bar(); }
};
Finalmente, ocultando las dos técnicas anteriores bajo un mismo tipo, se consigue proporcionar semántica por valor.
std::vector<Foo> foos;
foos.emplace_back(Foo1()); // Same hierarchy
foos.emplace_back(Foo2()); // Same hierarchy
foos.emplace_back(Foo3()); // Foreign class
for (auto const& foo : foos)
foo.bar();
I'm a Foo1 instance I'm a Foo2 instance I'm a Foo3 instance
When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.
James Whitcomb Riley
Optimización del objeto pequeño
Recursos de memoria polimórficos
Los mecanismos de implementación de la optimización del objeto pequeño y reserva de memoria de objetos con tipos borrados requieren de castings intensivos.
Las clases con métodos virtuales no poseen una disposición estándar (standard layout). Los castings directos de clase base a una localización de memoria donde se ha almacenado una clase derivada conducen a comportamiento indefinido.
De la herencia a las tablas de métodos... ¿cómo cambia el borrado de tipos?
class FooWrapper {
public:
using allocator_type = std::pmr::polymorphic_allocator<std::byte>;
private:
static std::size_t constexpr BUFFER_SIZE = 16;
allocator_type allocator_;
FooConcept const* dispatcher_;
union Storage {
std::aligned_storage_t<BUFFER_SIZE> local;
void* remote;
} storage_;
// ...
};
struct FooConcept {
void(*bar)(void* self);
void(*destroy)(void* self, void* alloc);
};
Implementando la tabla de punteros para los objetos pequeños...
template<typename T, typename U>
FooConcept const FooDispatcherSBO{
[](void* self) { // bar
auto storage = reinterpret_cast<T*>(&static_cast<U*>(self)->local);
storage->bar();
},
[](void* self, void*) { // destroy
auto storage = reinterpret_cast<T*>(&static_cast<U*>(self)->local);
std::destroy_at(storage);
}
};
Implementando la tabla de punteros para los objetos utilizando memoria dinámica...
template<typename T, typename U>
FooConcept const FooDispatcherPMR{
[](void* self) { // bar
auto storage = reinterpret_cast<T*>(static_cast<U*>(self)->remote);
remote->bar();
},
[](void* self, void* alloc) { // destroy
auto allocator = detail::rebind_function<T>(alloc);
auto storage = reinterpret_cast<T*>(static_cast<U*>(self)->remote);
if (storage)
std::destroy_at(storage);
allocator.deallocate(storage, 1);
}
};
class FooWrapper {
public:
template<typename T>
explicit FooWrapper(T&& obj, allocator_type alloc={})
: allocator_{ alloc }
{
std::pmr::polymorphic_allocator<T> allocator{ alloc };
T* storage;
if constexpr (sizeof(T) <= BUFFER_SIZE) {
dispatcher_ = &FooDispatcherSBO<T, Storage>;
storage = reinterpret_cast<T*>(&storage_.local);
}
else {
dispatcher_ = &FooDispatcherPMR<T, Storage>;
storage_.remote = allocator.allocate(1);
storage = reinterpret_cast<T*>(storage_.remote);
}
allocator.construct(storage, std::forward<T>(obj));
}
El desctructor y el método bar
son despachados a través de la tabla de punteros:
FooWrapper::~FooWrapper() {
dispatcher_->destroy(&storage_, &allocator_);
}
void FooWrapper::bar() const {
dispatcher_->bar(&const_cast<Storage&>(storage_));
}
Sea un objeto pequeño:
class SmallFoo {
public:
void bar() const { std::cout << "I am a small foo!" << std::endl; }
};
Sea un objeto que requiera de memoria dinámica:
class BigFoo {
char str_[24];
public:
BigFoo() noexcept { std::strcpy(str_, "I am a big foo!"); }
BigFoo(BigFoo const& other) noexcept : BigFoo{} {}
BigFoo(BigFoo&& other) noexcept : BigFoo{} {}
void bar() const { std::cout << str_ << std::endl; }
};
Ejemplo envolviendo ambos objetos en un vector que se aprovecha de un bloque de memoria local:
std::array<std::byte, 256> buffer;
std::pmr::monotonic_buffer_resource pool{ buffer.data(), buffer.size() };
std::pmr::vector<foo::FooWrapper> foos{ &pool };
foos.reserve(2); // No move-constructor defined yet
foos.emplace_back(SmallFoo());
foos.emplace_back(BigFoo());
for (auto const& foo : foos)
foo.bar();
Demostrado está, en C++ es posible un polimorfismo...
El ejemplo correcto y completo de borrado de tipos aquí presentado, utilizando optimización del objeto pequeño y recursos de memoria polimórficos (incluyendo los constructores de copia y movimiento, así como los operadores de asignación), puede encontrarse aquí.
Dave Kilian, C++ 'Type Erasure' Explained
José Daniel García, Polimorfismo estático y dinámico en C++11: ¿Flexibilidad contra rendimiento?
Sean Parent, Polymorphic Task
Sean Parent, Small Object Optimization for Polymorphic Types
Louis Dionne, Runtime Polymorphism: Back to the Basics