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ă:
#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
}Apasă Rulează pentru a compila și rula acest exemplu.
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), 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) 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:
#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
}Apasă Rulează pentru a compila și rula acest exemplu.
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
- 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?