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:
#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
}Pulsa Ejecutar para compilar y ejecutar este ejemplo.
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), 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) 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:
#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
}Pulsa Ejecutar para compilar y ejecutar este ejemplo.
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
- 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]]`?