368 views
# Entendiendo las diferencias entre Rust, C++ moderno y ANSI C con estructuras de datos recursivas Cuando aprendemos un nuevo lenguaje de programación, implementar una estructura de datos conocida, como un **árbol de expresiones matemáticas**, nos ayuda a entender cómo el lenguaje maneja la memoria, la seguridad y la ergonomía. En este documento, compararemos cómo Rust, C++ moderno y ANSI C abordan este problema clásico para evaluar expresiones como `(10 * 9) + ((3 - 4) * 5)`. ![](https://apuntes.grunt.ar/uploads/e26c2ccc-bbb8-4fe8-a523-3b4d99a9d0ee.png) --- ## 1. Rust: Seguridad y Ergonomía por Defecto 🦀 Rust está diseñado para prevenir errores desde la compilación, lo que lo hace ideal para estructuras recursivas. ### Tipos de Datos Usamos **enums** para definir una expresión, que puede ser un valor o una operación. Para la recursividad, `Box<T>` almacena datos en el heap, evitando problemas de tamaño infinito. ```rust /// Una operación a realizar. enum Operation { Add, Sub, Mul, Div, } /// Una expresión, en forma de árbol. enum Expression { /// Una operación sobre dos subexpresiones. Op { op: Operation, left: Box<Expression>, right: Box<Expression> }, /// Un valor literal. Value(i64), } ``` ### Lógica de Evaluación Con **match**, Rust ofrece un "pattern matching" exhaustivo que obliga a cubrir todos los casos, eliminando errores. ```rust fn eval(e: Expression) -> i64 { match e { Expression::Value(val) => val, Expression::Op { op, left, right } => { let left_val = eval(*left); let right_val = eval(*right); match op { Operation::Add => left_val + right_val, Operation::Sub => left_val - right_val, Operation::Mul => left_val * right_val, Operation::Div => left_val / right_val, } } } } ``` #### Conclusión Rust combina seguridad y expresividad con enums, `Box<T>` y `match`, guiando al programador hacia un código libre de errores comunes. --- ## 2. C++ Moderno: Poder y Flexibilidad con Abstracciones ⚙️ C++ moderno (C++17+) ofrece herramientas como `std::variant` y `std::unique_ptr`, acercándose a la seguridad de Rust, pero con más responsabilidad para el programador. ### Tipos de Datos `std::variant` actúa como un tipo suma seguro, mientras que `std::unique_ptr` gestiona la memoria automáticamente, similar a `Box<T>`. ```cpp #include <variant> #include <memory> enum class Operation { Add, Sub, Mul, Div }; struct Expression; // Declaración previa struct Op { Operation op; std::unique_ptr<Expression> left; std::unique_ptr<Expression> right; }; struct Value { long long val; }; struct Expression { std::variant<Value, Op> node; }; ``` ### Lógica de Evaluación `std::visit` permite operar sobre la variante, aunque es más verboso que el `match` de Rust. ```cpp long long eval(const Expression& e) { return std::visit( [](auto&& arg) -> long long { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, Value>) { return arg.val; } else if constexpr (std::is_same_v<T, Op>) { long long left_val = eval(*arg.left); long long right_val = eval(*arg.right); switch (arg.op) { case Operation::Add: return left_val + right_val; case Operation::Sub: return left_val - right_val; case Operation::Mul: return left_val * right_val; case Operation::Div: return left_val / right_val; } } return 0; // No debería ocurrir }, e.node); } ``` #### Conclusión C++ ofrece flexibilidad y seguridad con `std::variant` y `std::unique_ptr`, pero el programador debe usar estas herramientas conscientemente. --- ## 3. ANSI C: Control Total y Riesgo Máximo ☠️ En ANSI C, no hay abstracciones automáticas. El programador tiene control total, pero también toda la responsabilidad. ### Tipos de Datos Usamos una **unión etiquetada** (tagged union) con un enum y una union. La recursividad se maneja con punteros crudos (`*`), y la memoria se gestiona manualmente con `malloc` y `free`. ```c #include <stdlib.h> typedef enum { TYPE_VALUE, TYPE_OP } ExpressionType; typedef enum { OP_ADD, OP_SUB, OP_MUL, OP_DIV } Operation; typedef struct Expression Expression; // Declaración previa typedef struct { Operation op; Expression* left; Expression* right; } OpNode; struct Expression { ExpressionType type; union { long long value; OpNode op; } data; }; ``` ### Lógica de Evaluación Un `switch` actúa como "pattern matching". La gestión de memoria requiere una función `free_expression` para evitar fugas. ```c long long eval(const Expression* e) { switch (e->type) { case TYPE_VALUE: return e->data.value; case TYPE_OP: { long long left_val = eval(e->data.op.left); long long right_val = eval(e->data.op.right); switch (e->data.op.op) { case OP_ADD: return left_val + right_val; case OP_SUB: return left_val - right_val; case OP_MUL: return left_val * right_val; case OP_DIV: return left_val / right_val; } } } return 0; // No debería ocurrir } void free_expression(Expression* e) { if (e && e->type == TYPE_OP) { free_expression(e->data.op.left); free_expression(e->data.op.right); } free(e); } int main() { // Construcción del árbol: (10 * 9) + ((3 - 4) * 5) Expression* term1 = create_op(OP_MUL, create_value(10), create_value(9)); Expression* sub_term = create_op(OP_SUB, create_value(3), create_value(4)); Expression* term2 = create_op(OP_MUL, sub_term, create_value(5)); Expression* root = create_op(OP_ADD, term1, term2); printf("El resultado es: %lld\n", eval(root)); // Imprime 85 // Liberar la memoria del árbol free_expression(root); return 0; } ``` #### Conclusión C ofrece control y eficiencia, pero la gestión manual de memoria y la falta de seguridad lo hacen propenso a errores. --- ## Conclusión General - **Rust**: Seguridad y ergonomía con enums y `Box<T>`. - **C++ moderno**: Flexibilidad con `std::variant` y `std::unique_ptr`, pero requiere cuidado. - **ANSI C**: Control total con punteros y gestión manual, a costa de seguridad. Estos enfoques reflejan las filosofías de cada lenguaje, útiles según el contexto del proyecto.