Devolución de interfaces mediante punteros inteligentes
En el apartado anterior se analizó por qué devolver tipos concretos conduce a un código fuertemente acoplado, difícil de extender y de mantener. También se mostró que, aunque devolver objetos polimórficos parece una alternativa más flexible, en C++ presenta problemas técnicos como el object slicing, las referencias colgantes y la gestión manual de memoria.
La solución moderna y segura es devolver interfaces abstractas a través de punteros inteligentes. De este modo, una función puede crear un objeto de una clase concreta y devolverlo como interfaz base, ocultando completamente los detalles de su implementación y garantizando una gestión automática de los recursos.
Recordemos que uno de los principios fundamentales del diseño orientado a objetos es “programar contra interfaces, no contra implementaciones”. Cuando una función devuelve una interfaz —y no una clase concreta— el código cliente no recibe lo que el objeto es, sino lo que el objeto puede hacer: las operaciones definidas en la interfaz. Esto permite trabajar con diferentes tipos de objetos de manera uniforme, sin conocer su clase real, manteniendo así bajo acoplamiento y alta extensibilidad.
Este enfoque combina tres ideas esenciales del diseño en C++ moderno:
- Polimorfismo dinámico: el comportamiento se resuelve en tiempo de ejecución según el tipo real del objeto.
- RAII y punteros inteligentes: los recursos se gestionan automáticamente sin intervención del programador.
- Desacoplamiento: el cliente trabaja con la interfaz abstracta, sin depender de las clases concretas.
De esta manera, las funciones pueden crear y devolver objetos de distintos tipos de forma segura, flexible y coherente con los principios del diseño orientado a objetos.
Devolver una interfaz mediante std::unique_ptr
Cuando el objeto devuelto tiene una única propiedad (nadie más necesita compartirlo), se utiliza std::unique_ptr. Este tipo de puntero inteligente transfiere la propiedad de manera exclusiva y destruye automáticamente el objeto al salir del ámbito.
#include <iostream>
#include <memory>
// Interfaz base
class Forma {
public:
virtual void dibujar() const = 0;
virtual ~Forma() = default;
};
// Implementaciones concretas
class Circulo : public Forma {
public:
void dibujar() const override {
std::cout << "Dibujando un círculo\n";
}
};
class Rectangulo : public Forma {
public:
void dibujar() const override {
std::cout << "Dibujando un rectángulo\n";
}
};
// Función que devuelve la interfaz
std::unique_ptr<Forma> crearForma(bool esCirculo) {
if (esCirculo)
return std::make_unique<Circulo>();
else
return std::make_unique<Rectangulo>();
}
int main() {
auto forma1 = crearForma(true); // Crea un círculo
auto forma2 = crearForma(false); // Crea un rectángulo
forma1->dibujar(); // Llamada polimórfica: Circulo::dibujar()
forma2->dibujar(); // Llamada polimórfica: Rectangulo::dibujar()
}
- La función
crearForma()devuelve unstd::unique_ptr<Forma>, ocultando el tipo concreto del objeto creado. - El cliente recibe un puntero a la interfaz
Formay puede usarlo sin conocer si se trata de unCirculoo unRectangulo. - El polimorfismo dinámico garantiza que la llamada a
dibujar()invoque la implementación adecuada. - Gracias a RAII, el objeto se destruye automáticamente cuando el puntero sale del ámbito, sin necesidad de
delete.
Devolver una interfaz mediante std::shared_ptr
En algunos escenarios, varios componentes pueden necesitar compartir la misma instancia polimórfica (por ejemplo, un recurso gráfico o un servicio común).
En estos casos, se utiliza std::shared_ptr, que mantiene un conteo de referencias y destruye el objeto cuando la última referencia desaparece.
#include <iostream>
#include <memory>
// Interfaz base
class Forma {
public:
virtual void dibujar() const = 0;
virtual ~Forma() = default;
};
// Implementaciones concretas
class Circulo : public Forma {
public:
void dibujar() const override {
std::cout << "Dibujando un círculo\n";
}
};
class Rectangulo : public Forma {
public:
void dibujar() const override {
std::cout << "Dibujando un rectángulo\n";
}
};
// Función que devuelve una interfaz compartida
std::shared_ptr<Forma> crearForma(bool esCirculo) {
if (esCirculo)
return std::make_shared<Circulo>();
else
return std::make_shared<Rectangulo>();
}
int main() {
auto forma1 = crearForma(true); // Círculo
auto forma2 = crearForma(false); // Rectángulo
// Dos punteros compartiendo el mismo objeto
auto formaCompartida = forma1;
forma1->dibujar(); // Circulo
forma2->dibujar(); // Rectangulo
std::cout << "Referencias compartidas al círculo: "
<< forma1.use_count() << "\n";
}
crearForma()devuelve unstd::shared_ptr<Forma>, lo que permite compartir la misma instancia entre varios punteros.- Cada vez que se copia el puntero, el contador interno aumenta; cuando todas las referencias desaparecen, el objeto se destruye automáticamente.
- Esto es útil para recursos compartidos entre múltiples componentes.
- El método
use_count()permite consultar cuántas referencias apuntan al mismo objeto.