Vai al contenuto

Funzioni e Passaggio dei Parametri

Padroneggia i diversi modi di passare argomenti in C++, comprendi l'ottimizzazione del valore di ritorno, l'overloading delle funzioni e gli attributi moderni come [[nodiscard]].

Le Funzioni: L'Astrazione Fondamentale

Le funzioni sono l'unità fondamentale di organizzazione del codice in C++. A differenza di molti altri linguaggi, il C++ ti dà un controllo granulare su come gli argomenti vengono passati e restituiti dalle funzioni. Questo controllo non è solo zucchero sintattico — ha reali implicazioni sulle prestazioni.

Il modo in cui passi i parametri determina se la funzione lavora con una copia dell'argomento, un riferimento all'originale, o un puntatore all'originale. Scegliere il meccanismo giusto è una delle decisioni più importanti quando si progetta l'interfaccia di una funzione.

Meccanismi di Passaggio dei Parametri

Il C++ supporta quattro modi principali per passare gli argomenti. Ciascuno ha compromessi diversi in termini di prestazioni, sicurezza e 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
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Buona pratica

Segui queste linee guida dalle C++ Core Guidelines per scegliere come passare i parametri:

- Economico da copiare e mai modificato? Passa per valore (int, double, char, bool, struct piccole, std::string_view).
- Costoso da copiare e solo letto? Passa per const& (std::string, std::vector, oggetti grandi).
- Necessario modificare la variabile del chiamante? Passa per & (riferimento non-const).
- Potrebbe essere null? Passa per puntatore. Ma considera std::optional& come alternativa.
- Acquisizione della proprietà (sink)? Passa per valore e sposta (trattato nel modulo sulla semantica di spostamento).

Come regola pratica, gli oggetti più grandi di 2-3 puntatori (16-24 byte su 64 bit) dovrebbero essere passati per riferimento piuttosto che per valore.

  • Passa i tipi piccoli ed economici per valore: int, double, char, bool, string_view
  • Passa i grandi oggetti in sola lettura per riferimento const: const std::string&, const std::vector&
  • Passa per riferimento non-const solo quando la funzione deve modificare l'oggetto del chiamante
  • Passa per puntatore quando nullptr è un argomento valido — ma preferisci std::optional dove possibile
  • In caso di dubbio, passa per riferimento const — è sicuro ed efficiente

Ottimizzazione del Valore di Ritorno (RVO/NRVO)

Una preoccupazione comune per i principianti è che restituire oggetti grandi per valore sia costoso a causa della copia. Nel C++ moderno, questa preoccupazione è quasi sempre infondata grazie alla Return Value Optimization (RVO) e alla Named Return Value Optimization (NRVO).

RVO (garantita dal C++17): Quando una funzione restituisce un temporaneo (es. return std::vector{1,2,3};), il compilatore deve costruire l'oggetto direttamente nella memoria del chiamante. Nessuna copia o spostamento avviene.

NRVO (permessa, non garantita): Quando una funzione restituisce una variabile locale con nome (es. return result;), il compilatore può costruire quella variabile direttamente nella memoria del chiamante. Tutti i principali compilatori lo fanno nella pratica, anche in modalità debug.

Il consiglio pratico: restituisci per valore liberamente. Non usare parametri di output (void f(std::vector& out)) per evitare copie — lascia che il compilatore ottimizzi il ritorno.

Overloading delle Funzioni e Argomenti Predefiniti

Il C++ consente più funzioni con lo stesso nome ma liste di parametri diverse. Il compilatore seleziona il miglior match basandosi sugli argomenti forniti al punto di chiamata:

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
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Insidia

La risoluzione degli overload delle funzioni segue regole complesse, e le chiamate ambigue sono una fonte comune di errori di compilazione. Fai attenzione a queste insidie:

1. Le conversioni implicite creano ambiguità: Se hai f(int) e f(double), chiamare f(3.14f) (un float) è ambiguo perché float si converte ugualmente bene sia in int che in double. Soluzione: aggiungi un overload f(float), o usa static_cast.

2. Gli argomenti predefiniti interagiscono male con gli overload: void f(int x, int y = 0) e void f(int x) creano ambiguità quando chiamati con un argomento.

3. Riferimento const vs valore: f(int) e f(const int&) sono ambigui per argomenti lvalue perché entrambi sono match ugualmente buoni.

Quando il compilatore segnala un overload ambiguo, non indovinare — leggi il messaggio di errore attentamente. Ti dirà quali overload sono candidati e perché sono ugualmente buoni.

  • Le conversioni implicite possono rendere ambigua la risoluzione degli overload — aggiungi overload espliciti per risolvere
  • Argomenti predefiniti e overload possono entrare in conflitto — evita entrambi sullo stesso nome di funzione
  • In caso di dubbio, il messaggio di errore del compilatore elenca gli overload candidati e l'ambiguità
  • Usa static_cast per selezionare esplicitamente un overload quando chiami con argomenti che corrispondono a più overload
Concetti chiave
  • Passa i tipi piccoli per valore, i tipi grandi in sola lettura per riferimento const, e i tipi mutabili per riferimento non-const
  • Restituisci per valore liberamente — RVO/NRVO elimina la copia nella pratica
  • L'overloading delle funzioni seleziona il miglior match basandosi sui tipi degli argomenti — attenzione all'ambiguità da conversioni implicite
  • Usa [[nodiscard]] sulle funzioni il cui valore di ritorno non deve essere ignorato (codici di errore, risultati calcolati)
  • Gli argomenti predefiniti offrono comodità ma possono entrare in conflitto con gli overload — usa l'uno o l'altro
  • inline permette di definire le funzioni negli header senza violare la One Definition Rule

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Quando dovresti passare un `std::vector<int>` per `const&` invece che per valore?

2. Cos'è la Return Value Optimization (RVO)?

3. Cosa fa l'attributo `[[nodiscard]]`?