Ir al contenido

Funciones y Paso de Parametros

Domina las diferentes formas de pasar argumentos en C++, comprende la optimizacion del valor de retorno, la sobrecarga de funciones y atributos modernos como [[nodiscard]].

Funciones: La Abstraccion Central

Las funciones son la unidad fundamental de organizacion de codigo en C++. A diferencia de muchos otros lenguajes, C++ te da control detallado sobre como los argumentos se pasan a las funciones y se retornan de ellas. Este control no es solo azucar sintactico — tiene implicaciones reales de rendimiento.

La forma en que pasas los parametros determina si la funcion trabaja con una copia del argumento, una referencia al original, o un puntero al original. Elegir el mecanismo correcto es una de las decisiones mas importantes al disenar la interfaz de una funcion.

Mecanismos de Paso de Parametros

C++ soporta cuatro formas principales de pasar argumentos. Cada una tiene diferentes compensaciones en rendimiento, seguridad y semantica:

parameter_passing.cppc++23
#include <iostream>
#include <string>
#include <vector>

// 1. Pass by VALUE — the function gets a copy
//    Use for: small types (int, double, char), when you need a local copy
void square(int x) {           // x is a copy of the argument
    x = x * x;                 // Modifying x does NOT affect the caller
    std::cout << "Inside: " << x << '\n';
}

// 2. Pass by REFERENCE — the function works on the original
//    Use for: when you need to modify the caller's variable
void double_it(int& x) {       // x IS the caller's variable
    x *= 2;                    // This DOES modify the caller's variable
}

// 3. Pass by CONST REFERENCE — read-only access to the original, no copy
//    Use for: large objects you only need to read
void print_all(const std::vector<std::string>& names) {
    for (const auto& name : names) {
        std::cout << name << '\n';
    }
    // names.push_back("Eve");  // ERROR: names is const
}

// 4. Pass by POINTER — explicit nullable indirection
//    Use for: optional parameters (can be nullptr), C API interop
void maybe_increment(int* ptr) {
    if (ptr != nullptr) {       // Must check for null!
        ++(*ptr);
    }
}

int main() {
    int val = 5;

    square(val);
    std::cout << "After square: " << val << '\n';      // Still 5 (copied)

    double_it(val);
    std::cout << "After double_it: " << val << '\n';   // Now 10 (modified)

    std::vector<std::string> people = {"Alice", "Bob", "Charlie"};
    print_all(people);         // No copy of the vector — efficient

    maybe_increment(&val);     // Pass address explicitly
    std::cout << "After maybe_increment: " << val << '\n';  // 11

    maybe_increment(nullptr);  // Safe: the function checks for null
}
Salida

Pulsa Ejecutar para compilar y ejecutar este ejemplo.

Buena práctica

Sigue estas guias de las C++ Core Guidelines para elegir como pasar parametros:

- Barato de copiar y nunca modificado? Pasa por valor (int, double, char, bool, structs pequenos, std::string_view).
- Costoso de copiar y solo se lee? Pasa por const& (std::string, std::vector, objetos grandes).
- Necesitas modificar la variable del llamador? Pasa por & (referencia no const).
- Podria ser nulo? Pasa por puntero. Pero considera std::optional& en su lugar.
- Tomando propiedad (sink)? Pasa por valor y mueve (cubierto en el modulo de semantica de movimiento).

Como regla general, los objetos mas grandes que 2-3 punteros (16-24 bytes en 64 bits) deben pasarse por referencia en lugar de por valor.

  • Pasa tipos pequenos y baratos por valor: int, double, char, bool, string_view
  • Pasa objetos grandes de solo lectura por referencia const: const std::string&, const std::vector&
  • Pasa por referencia no const solo cuando la funcion necesita modificar el objeto del llamador
  • Pasa por puntero cuando nullptr es un argumento valido — pero prefiere std::optional donde sea posible
  • Cuando tengas dudas, pasa por referencia const — es seguro y eficiente

Optimizacion del Valor de Retorno (RVO/NRVO)

Una preocupacion comun de los principiantes es que retornar objetos grandes por valor es costoso debido a la copia. En C++ moderno, esta preocupacion es casi siempre infundada gracias a la Optimizacion del Valor de Retorno (RVO) y la Optimizacion del Valor de Retorno con Nombre (NRVO).

RVO (garantizada desde C++17): Cuando una funcion retorna un temporal (por ejemplo, return std::vector{1,2,3};), el compilador debe construir el objeto directamente en la memoria del llamador. No ocurre ninguna copia ni movimiento.

NRVO (permitida, no garantizada): Cuando una funcion retorna una variable local con nombre (por ejemplo, return result;), el compilador puede construir esa variable directamente en la memoria del llamador. Todos los compiladores principales hacen esto en la practica, incluso en modo de depuracion.

El consejo practico: retorna por valor libremente. No uses parametros de salida (void f(std::vector& out)) para evitar copias — deja que el compilador optimice el retorno.

Sobrecarga de Funciones y Argumentos por Defecto

C++ permite multiples funciones con el mismo nombre pero diferentes listas de parametros. El compilador selecciona la mejor coincidencia basandose en los argumentos proporcionados en el sitio de la llamada:

overloading.cppc++23
#include <iostream>
#include <string>
#include <vector>

// Function overloading — same name, different parameter types/counts
void log(int value) {
    std::cout << "[INT] " << value << '\n';
}

void log(double value) {
    std::cout << "[DOUBLE] " << value << '\n';
}

void log(const std::string& value) {
    std::cout << "[STRING] " << value << '\n';
}

// Default arguments — provide fallback values
void greet(const std::string& name, const std::string& greeting = "Hello") {
    std::cout << greeting << ", " << name << "!\n";
}

// [[nodiscard]] (C++17) — warn if return value is ignored
[[nodiscard]] int compute(int a, int b) {
    return a * b + a;
}

// [[nodiscard]] with reason (C++20)
[[nodiscard("Ignoring error codes can hide failures")]]
int open_file(const std::string& path) {
    // Returns 0 on success, negative on error
    return 0;  // Simplified
}

// inline — suggests the compiler inline this function (also allows
// the function to be defined in a header without ODR violations)
inline int fast_max(int a, int b) {
    return (a > b) ? a : b;
}

int main() {
    log(42);               // Calls log(int)
    log(3.14);             // Calls log(double)
    log(std::string{"hi"});// Calls log(const string&)

    greet("Alice");                // Uses default: "Hello, Alice!"
    greet("Bob", "Good morning"); // Uses provided: "Good morning, Bob!"

    // int result = compute(3, 4);  // OK
    // compute(3, 4);               // WARNING: ignoring [[nodiscard]] return value

    int status = open_file("data.txt");  // OK: return value used
}
Salida

Pulsa Ejecutar para compilar y ejecutar este ejemplo.

Error común

La resolucion de sobrecarga de funciones sigue reglas complejas, y las llamadas ambiguas son una fuente comun de errores de compilacion. Ten en cuenta estas trampas:

1. Las conversiones implicitas crean ambiguedad: Si tienes f(int) y f(double), llamar a f(3.14f) (un float) es ambiguo porque float se convierte igualmente bien a int y a double. La solucion: agrega una sobrecarga f(float), o usa static_cast.

2. Los argumentos por defecto interactuan mal con las sobrecargas: void f(int x, int y = 0) y void f(int x) crean ambiguedad cuando se llaman con un argumento.

3. Referencia const vs valor: f(int) y f(const int&) son ambiguas para argumentos lvalue porque ambas son coincidencias igualmente buenas.

Cuando el compilador reporta una sobrecarga ambigua, no adivines — lee el mensaje de error cuidadosamente. Te dira que sobrecargas son candidatas y por que son igualmente buenas.

  • Las conversiones implicitas pueden hacer ambigua la resolucion de sobrecarga — agrega sobrecargas explicitas para resolver
  • Los argumentos por defecto y las sobrecargas pueden entrar en conflicto — evita ambos en el mismo nombre de funcion
  • Cuando tengas dudas, el mensaje de error del compilador lista las sobrecargas candidatas y la ambiguedad
  • Usa static_cast para seleccionar explicitamente una sobrecarga al llamar con argumentos que coinciden con multiples sobrecargas
Puntos clave
  • Pasa tipos pequenos por valor, tipos grandes de solo lectura por referencia const, y tipos mutables por referencia no const
  • Retorna por valor libremente — RVO/NRVO elimina la copia en la practica
  • La sobrecarga de funciones selecciona la mejor coincidencia basandose en los tipos de argumento — cuidado con la ambiguedad de las conversiones implicitas
  • Usa [[nodiscard]] en funciones cuyo valor de retorno no debe ignorarse (codigos de error, resultados calculados)
  • Los argumentos por defecto proporcionan conveniencia pero pueden entrar en conflicto con las sobrecargas — usa uno u otro
  • inline permite que las funciones se definan en headers sin violar la Regla de Una Definicion

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Cuando deberias pasar un `std::vector<int>` por `const&` en lugar de por valor?

2. Que es la Optimizacion del Valor de Retorno (RVO)?

3. Que hace el atributo `[[nodiscard]]`?