Sari la conținut

Functions & Parameter Passing

Master the different ways to pass arguments in C++, understand return value optimization, function overloading, and modern attributes like [[nodiscard]].

Functions: The Core Abstraction

Functions are the fundamental unit of code organization in C++. Unlike many other languages, C++ gives you fine-grained control over how arguments are passed to and returned from functions. This control is not just syntactic sugar — it has real performance implications.

The way you pass parameters determines whether the function works with a copy of the argument, a reference to the original, or a pointer to the original. Choosing the right mechanism is one of the most important decisions you make when designing a function's interface.

Parameter Passing Mechanisms

C++ acceptă patru moduri principale de a transmite argumente. Fiecare are compromisuri diferite în ceea ce privește performanța, siguranță și semantică:

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

Apasă Rulează pentru a compila și rula acest exemplu.

Bună practică

Urmați aceste instrucțiuni din Ghidurile de bază C++ pentru a alege cum să transmiteți parametrii:

- Ieftin de copiat și nu a fost niciodată modificat? Treci după valoare (int,double,char,bool, structuri mici,std::string_view).
- Scusent de copiat și doar de citit? Treceți pe lângăconst&(std::string,std::vector, obiecte mari).
- Trebuie să modificați variabila apelantului? Treceți de&(referință non-const).
- Ar putea fi nul? Treci prin indicator. Dar luați în considerare în schimbstd::optional&.
- Preluarea proprietății (scufundarea)? Treceți prin valoare și mutați (acoperit în modulul de semantică a mișcării).

Ca regulă generală, obiectele mai mari de 2-3 pointeri (16-24 de octeți pe 64 de biți) ar trebui să fie transmise prin referință și nu prin valoare.

  • Transmite tipuri mici, ieftine după valoare: int, double, char, bool, string_view
  • Treceți obiecte mari numai în citire prin referință const: const std::string&, const std::vector&
  • Treceți prin referință non-const numai atunci când funcția trebuie să modifice obiectul apelantului
  • Treceți prin indicator când nullptr este un argument valid - dar preferați std::opțional acolo unde este posibil
  • Când aveți îndoieli, treceți prin referință constantă - este sigur și eficient

Optimizarea valorii de returnare (RVO/NRVO)

O preocupare comună pentru începători este că returnarea obiectelor mari după valoare este costisitoare din cauza copierii. În C++ modern, această preocupare este aproape întotdeauna nefondată datorită Return Value Optimization (RVO) și Named Return Value Optimization (NRVO).

RVO (garantat de la C++17): Când o funcție returnează un temporar (de exemplu,return std::vector{1,2,3};), compilatorul trebuie să construiască obiectul direct în memoria apelantului. Nu are loc nicio copiere sau mutare.

NRVO (permis, nu garantat): Când o funcție returnează o variabilă locală numită (de exemplu,return result;), compilatorul poate construi acea variabilă direct în memoria apelantului. Toate compilatoarele majore fac acest lucru în practică, chiar și în modul de depanare.

Sfatul practic: reveniți după valoare în mod gratuit. Nu utilizați parametrii de ieșire (void f(std::vector& out)) pentru a evita copiile - lăsați compilatorul să optimizeze returnarea.

Supraîncărcarea funcției și argumentele implicite

C++ permite mai multe funcții cu același nume, dar liste de parametri diferite. Compilatorul selectează cea mai bună potrivire pe baza argumentelor furnizate pe site-ul de apel:

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

Apasă Rulează pentru a compila și rula acest exemplu.

Capcană

Rezoluția supraîncărcării funcției urmează reguli complexe, iar apelurile ambigue sunt o sursă comună de erori de compilare. Fiți conștienți de aceste capcane:

1. Conversiile implicite creează ambiguitate: dacă avețif(int)șif(double), apelareaf(3.14f)(unfloat) este ambiguă, deoarecefloatconvertește la fel de bine atât laint, cât și ladouble. Remedierea: adăugați o supraîncărcaref(float)sau utilizațistatic_cast.

2. Argumentele implicite interacționează slab cu supraîncărcările:void f(int x, int y = 0)șivoid f(int x)creează ambiguitate atunci când sunt apelate cu un singur argument.

3. referință constantă vs valoare:f(int)șif(const int&)sunt ambigue pentru argumentele lvalue, deoarece ambele sunt potriviri la fel de bune.

Când compilatorul raportează o supraîncărcare ambiguă, nu ghiciți - citiți cu atenție mesajul de eroare. Vă va spune ce supraîncărcări sunt candidați și de ce sunt la fel de bune.

  • Conversiile implicite pot face rezoluția supraîncărcării ambiguă - adăugați supraîncărcări explicite pentru a le rezolva
  • Argumentele implicite și supraîncărcările pot intra în conflict - evitați ambele cu același nume de funcție
  • Când aveți îndoieli, mesajul de eroare al compilatorului listează supraîncărcările candidatului și ambiguitatea
  • Utilizați static_cast pentru a selecta în mod explicit o supraîncărcare atunci când apelați cu argumente care se potrivesc cu mai multe supraîncărcări
Idei cheie
  • Transmiteți tipurile mici după valoare, tipurile mari numai pentru citire prin referință constantă și tipurile mutabile prin referință non-const
  • Întoarcere gratuită după valoare — RVO/NRVO elimină copia în practică
  • Supraîncărcarea funcției selectează cea mai bună potrivire în funcție de tipurile de argumente - ferește-te de ambiguitatea din conversiile implicite
  • Utilizați [[nodiscard]] pentru funcțiile a căror valoare returnată nu trebuie ignorată (coduri de eroare, rezultate calculate)
  • Argumentele implicite oferă comoditate, dar pot intra în conflict cu supraîncărcările - utilizați unul sau altul
  • inline permite ca funcțiile să fie definite în anteturi fără a încălca Regula de definiție unică

Test — Verifică-ți cunoștințele

(15 XP)

1. Când ar trebui să treceți un`std::vector<int>`cu`const&`și nu după valoare?

2. Ce este optimizarea valorii de returnare (RVO)?

3. What does the`[[nodiscard]]`attribute do?