Fonctions et passage de paramètres
Maîtrisez les différentes façons de passer des arguments en C++, comprenez l'optimisation de la valeur de retour, la surcharge de fonctions, et les attributs modernes comme [[nodiscard]].
Les fonctions : l'abstraction fondamentale
Les fonctions sont l'unité fondamentale d'organisation du code en C++. Contrairement à de nombreux autres langages, le C++ vous donne un contrôle granulaire sur la façon dont les arguments sont passés aux fonctions et retournés par celles-ci. Ce contrôle n'est pas juste du sucre syntaxique — il a de vraies implications sur les performances.
La façon dont vous passez les paramètres détermine si la fonction travaille avec une copie de l'argument, une référence vers l'original, ou un pointeur vers l'original. Choisir le bon mécanisme est l'une des décisions les plus importantes que vous prenez en concevant l'interface d'une fonction.
Mécanismes de passage de paramètres
Le C++ supporte quatre façons principales de passer des arguments. Chacune a des compromis différents en termes de performance, sécurité et sémantique :
#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
}Cliquez sur Exécuter pour compiler et lancer cet exemple.
Suivez ces directives des C++ Core Guidelines pour choisir comment passer les paramètres :
- Peu coûteux à copier et jamais modifié ? Passez par valeur (int, double, char, bool, petites structs, std::string_view).
- Coûteux à copier et seulement lu ? Passez par const& (std::string, std::vector, gros objets).
- Besoin de modifier la variable de l'appelant ? Passez par & (référence non-const).
- Pourrait être nul ? Passez par pointeur. Mais envisagez std::optional à la place.
- Prise de possession (sink) ? Passez par valeur et déplacez (traité dans le module sur la sémantique de déplacement).
En règle générale, les objets plus grands que 2-3 pointeurs (16-24 octets sur du 64 bits) devraient être passés par référence plutôt que par valeur.
- Passez les petits types peu coûteux par valeur : int, double, char, bool, string_view
- Passez les gros objets en lecture seule par référence constante : const std::string&, const std::vector
& - Passez par référence non-const uniquement quand la fonction doit modifier l'objet de l'appelant
- Passez par pointeur quand nullptr est un argument valide — mais préférez std::optional quand c'est possible
- En cas de doute, passez par référence constante — c'est sûr et efficace
Optimisation de la valeur de retour (RVO/NRVO)
Une préoccupation courante pour les débutants est que retourner de gros objets par valeur est coûteux à cause de la copie. En C++ moderne, cette préoccupation est presque toujours infondée grâce à la Return Value Optimization (RVO) et la Named Return Value Optimization (NRVO).
RVO (garantie depuis C++17) : Quand une fonction retourne un temporaire (par ex., return std::vector), le compilateur doit construire l'objet directement dans la mémoire de l'appelant. Aucune copie ni déplacement ne se produit.
NRVO (autorisée, non garantie) : Quand une fonction retourne une variable locale nommée (par ex., return result;), le compilateur peut construire cette variable directement dans la mémoire de l'appelant. Tous les compilateurs majeurs le font en pratique, même en mode débogage.
Le conseil pratique : retournez par valeur librement. N'utilisez pas de paramètres de sortie (void f(std::vector) pour éviter les copies — laissez le compilateur optimiser le retour.
Surcharge de fonctions et arguments par défaut
Le C++ permet plusieurs fonctions avec le même nom mais des listes de paramètres différentes. Le compilateur sélectionne la meilleure correspondance en se basant sur les arguments fournis au site d'appel :
#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
}Cliquez sur Exécuter pour compiler et lancer cet exemple.
La résolution de surcharge de fonctions suit des règles complexes, et les appels ambigus sont une source courante d'erreurs de compilation. Soyez attentif à ces pièges :
1. Les conversions implicites créent de l'ambiguïté : Si vous avez f(int) et f(double), appeler f(3.14f) (un float) est ambigu car float se convertit aussi bien en int qu'en double. La solution : ajoutez une surcharge f(float), ou utilisez static_cast.
2. Les arguments par défaut interagissent mal avec les surcharges : void f(int x, int y = 0) et void f(int x) créent une ambiguïté lorsqu'on appelle avec un seul argument.
3. Référence const vs valeur : f(int) et f(const int&) sont ambigus pour les arguments lvalue car les deux sont des correspondances également bonnes.
Quand le compilateur signale une surcharge ambiguë, ne devinez pas — lisez attentivement le message d'erreur. Il vous indiquera quelles surcharges sont candidates et pourquoi elles sont également bonnes.
- Les conversions implicites peuvent rendre la résolution de surcharge ambiguë — ajoutez des surcharges explicites pour résoudre
- Les arguments par défaut et les surcharges peuvent entrer en conflit — évitez les deux sur le même nom de fonction
- En cas de doute, le message d'erreur du compilateur liste les surcharges candidates et l'ambiguïté
- Utilisez static_cast pour sélectionner explicitement une surcharge lors d'un appel avec des arguments correspondant à plusieurs surcharges
- Passez les petits types par valeur, les gros types en lecture seule par référence constante, et les types modifiables par référence non-const
- Retournez par valeur librement — RVO/NRVO élimine la copie en pratique
- La surcharge de fonctions sélectionne la meilleure correspondance en se basant sur les types des arguments — méfiez-vous de l'ambiguïté due aux conversions implicites
- Utilisez [[nodiscard]] sur les fonctions dont la valeur de retour ne doit pas être ignorée (codes d'erreur, résultats calculés)
- Les arguments par défaut offrent de la commodité mais peuvent entrer en conflit avec les surcharges — utilisez l'un ou l'autre
- inline permet de définir des fonctions dans les en-têtes sans violer la règle de définition unique
Quiz — Testez vos connaissances
(15 XP)1. Quand devriez-vous passer un `std::vector<int>` par `const&` plutôt que par valeur ?
2. Qu'est-ce que la Return Value Optimization (RVO) ?
3. Que fait l'attribut `[[nodiscard]]` ?