Une fonction spécifie une action ou un calcul qui fait passer le système d’un état cohérent à un autre. C’est le bloc de construction fondamental des programmes.
Il doit être possible de nommer une fonction de manière significative, de spécifier les exigences de son argument et d’énoncer clairement la relation entre les arguments et le résultat. Une implémentation n’est pas une spécification. Essayez de penser à ce que fait une fonction autant qu’à comment elle le fait.
Les fonctions sont la partie la plus critique dans la plupart des interfaces, consultez donc les règles d’interface.
constexprinlinenoexceptT* ou T& plutôt que des pointeurs intelligentsconstconstX&& et std::move le paramètreTP&& et ne faire std::forward que le paramètreT* sur T& lorsque « aucun argument » est une option valideT* ou owner<T*> pour désigner un seul objetnot_null<T> pour indiquer que « nul » n’est pas une valeur validespan<T> ou un span_p<T> pour désigner une séquence semi‑ouvertezstring ou not_null<zstring> pour désigner une chaîne C‑styleunique_ptr<T> pour transférer la propriété lorsqu’un pointeur est nécessaireshared_ptr<T> pour partager la propriétéRègles sémantiques de retour de valeur :
T* pour indiquer une position (uniquement)T& quand la copie est indésirable et qu’un retour d’un objet vide n’est pas requisT&&int est le type de retour pour main()T& depuis les opérateurs d’affectationstd::move(local)const Tthis ou tout membre de classe, ne pas utiliser la capture par défaut [=]va_argLes fonctions présentent de fortes similitudes avec les lambdas et les objets fonction.
Voir aussi : C.lambdas : Objets fonction et lambdas
Une définition de fonction est une déclaration de fonction qui spécifie également la mise en œuvre de la fonction, le corps de la fonction.
Extraits de code communs rendent le code plus lisible, plus susceptible d’être réutilisé et limitent les erreurs provenant d’un code complexe. Si quelque chose constitue une action bien définie, séparez‑le du code environnant et donnez‑lui un nom.
void read_and_print(istream& is) // lit un int et l’affiche
{
int x;
if (is >> x)
cout << "the int is " << x << '\n';
else
cerr << "no int on input\n";
}
Tout ce qui est mal avec read_and_print.
Il lit, écrit (sur un ostream fixe), écrit des messages d’erreur (sur un ostream fixe) et ne gère que les int.
Il n’y a rien à réutiliser, les opérations logiquement séparées sont mêlées et les variables locales restent dans le scope après l’usage logique.
Pour un petit exemple, cela semble correct, mais si l’opération d’entrée, de sortie et la gestion des erreurs étaient plus complexes, ce désordre pourrait devenir difficile à comprendre.
Si vous écrivez un lambda non trivial qu’il est possible d’utiliser à plus d’un endroit, donnez‑lui un nom en l’affectant à une variable (généralement non locale).
sort(a, b, [](T x, T y) { return x.rank() < y.rank() && x.value() < y.value(); });
Nommer ce lambda décortique l’expression en ses parties logiques et fournit un indice fort sur la signification du lambda.
auto lessT = [](T x, T y) { return x.rank() < y.rank() && x.value() < y.value(); };
sort(a, b, lessT);
Le code le plus court n’est pas toujours le meilleur pour la performance ou la maintenabilité.
Les corps de boucle, y compris les lambdas utilisés comme corps de boucle, ont rarement besoin d’être nommés.
Cependant, de gros corps de boucle (par ex. des dizaines de lignes ou de pages) peuvent poser problème.
La règle Garder les fonctions courtes et simples implique « Garder les corps de boucle courts ».
De même, les lambdas utilisés comme arguments de rappel sont parfois non triviales, mais peu susceptibles d’être réutilisées.
Une fonction qui exécute une seule opération est plus simple à comprendre, à tester et à réutiliser.
Considérons :
void read_and_print() // mauvais
{
int x;
cin >> x;
// check for errors
cout << x << "\n";
}
C’est un monolithe lié à une saisie spécifique et ne trouvera jamais un autre (différent) usage.
Aoûtant des fonctions adaptées aux parties logiques et les paramétrer :
int read(istream& is) // mieux
{
int x;
is >> x;
// check for errors
return x;
}
void print(ostream& os, int x)
{
os << x << "\n";
}
Elles peuvent maintenant être combinées où nécessaire :
void read_and_print()
{
auto x = read(cin);
print(cout, x);
}
Si besoin, on peut encore génreraliser read() et print() sur le type de données, le mécanisme de I/O, la réponse aux erreurs, etc. Exemple :
auto read = [](auto& input, auto& value) // mieux
{
input >> value;
// check for errors
};
void print(auto& output, const auto& value)
{
output << value << "\n";
}
tuple, pour plusieurs valeurs de retour.Les fonctions grandes sont difficiles à lire, plus susceptibles de contenir du code complexe et plus susceptibles de comporter des variables avec des écopes plus larges que le minimum.
Les fonctions maîtrisées la structure de contrôle, sont plus susceptibles d’augmenter et d’essayer d’enfreindre les erreurs logiques.
Considérez :
double simple_func(double val, int flag1, int flag2)
// simple_func : prend une valeur et calcule la sortie attendue ASIC
// en fonction des deux indicateurs de mode.
{
double intermediate;
if (flag1 > 0) {
intermediate = func1(val);
if (flag2 % 2)
intermediate = sqrt(intermediate);
}
else if (flag1 == -1) {
intermediate = func1(-val);
if (flag2 % 2)
intermediate = sqrt(-intermediate);
flag1 = -flag1;
}
if (abs(flag2) > 10) {
intermediate = func2(intermediate);
}
switch (flag2 / 10) {
case 1: if (flag1 == -1) return finalize(intermediate, 1.171);
break;
case 2: return finalize(intermediate, 13.1);
default: break;
}
return finalize(intermediate, 0.);
}
C’est trop compliqué.
Comment savoir si toutes les alternatives possibles ont été correctement traitées ?
Oui, cela enfreint d’autres règles également.
On peut réfactorer :
double func1_muon(double val, int flag)
{
// ???
}
double func1_tau(double val, int flag1, int flag2)
{
// ???
}
double simple_func(double val, int flag1, int flag2)
// simple_func : prend une valeur et calcule la sortie attendue ASIC
// en fonction des deux indicateurs de mode.
{
if (flag1 > 0)
return func1_muon(val, flag2);
if (flag1 == -1)
// géré par func1_tau : flag1 = -flag1;
return func1_tau(-val, flag1, flag2);
return 0.;
}
« Ne dépasse pas une fenêtre » constitue souvent une bonne définition pratique de « trop gros ».
Les fonctions d’une à cinq lignes sont généralement considérées comme normales.
Diviser les fonctions grandes en fonctions plus petites cohérentes et nommées.
Les petites fonctions simples sont facilement en‑ligne là où le coût d’un appel de fonction est important.
switch simple comme un chemin.constexprconstexpr est nécessaire pour dire au compilateur d’autoriser l’évaluation à la compilation.
Le (infâme) factoriel :
constexpr int fac(int n)
{
constexpr int max_exp = 17; // constexpr permet d’utiliser max_exp dans Expects
Expects(0 <= n && n < max_exp); // évite la folie et le dépassement
int x = 1;
for (int i = 2; i <= n; ++i) x *= i;
return x;
}
C’est C++14.
Pour C++11, utilisez une formulation récursive de fac().
constexpr n’assure pas une évaluation à la compilation ; il assure simplement qu’une fonction peut être évaluée à la compilation pour des arguments d’expression constante si le programmeur le demande ou si le compilateur décide de le faire pour optimiser.
constexpr int min(int x, int y) { return x < y ? x : y; }
void test(int v)
{
int m1 = min(-1, 2); // probablement évaluation à la compilation
constexpr int m2 = min(-1, 2); // évaluation à la compilation
int m3 = min(-1, v); // évaluation à l’exécution
constexpr int m4 = min(-1, v); // erreur : impossible d’évaluer à la compilation
}
N’essayez pas de rendre toutes les fonctions constexpr.
La plupart des calculs sont mieux faits à l’exécution.
Tout API qui pourrait en fin de compte dépendre d’une configuration ou de logique métier à l’exécution ne doit pas être rendu constexpr.
Une telle personnalisation ne peut être évaluée par le compilateur, et toute fonction constexpr qui dépendait de cette API devrait être refactorisée ou abandonner constexpr.
Inévitable et inutile.
Le compilateur donne une erreur si une fonction non constexpr est appelée là où une valeur constante est requise.
inlineCertains optimiseurs sont bons pour inline sans l’indication du programmeur, mais ne comptez pas sur eux.
Mesurez ! Au cours des 40 dernières années, on a promis des compilateurs qui inlineaient mieux que les humains sans aucune indication.
On attend encore.
Spécifier inline (explicitement, ou implicitement lorsqu’on écrit une fonction membre dans la définition d’une classe) encourage le compilateur à faire un meilleur rendu.
inline string cat(const string& s, const string& s2) { return s + s2; }
Ne pas placer une fonction inline dans ce qui est censé être une interface stable à moins d’être sûr qu’elle ne changera pas.
Une fonction inline fait partie de l’ABI.
constexpr implique inline.
Les fonctions membres définies dans la définition d’une classe sont inline par défaut.
Les modèles de fonctions (y compris les fonctions membres de modèles de classes A<T>::function() et les fonctions de modèle de classe A::function<T>()) sont normalement définis dans les en‑têtes et donc inline.
Considérez de faire sortir des fonctions qui ont plus de trois instructions et qui peuvent être déclarées hors ligne.
noexceptS’il n’est pas supposé lancer une exception, le programme ne peut pas supposer qu’il peut gérer l’erreur et doit être terminé dès que possible.
Déclarer une fonction noexcept aide les optimiseurs en réduisant le nombre de chemins d’exécution alternatifs.
C’est aussi plus rapide lors de la sortie après l’échec.
Mettre noexcept sur chaque fonction écrite complètement en C ou dans n’importe quelle autre langue sans exceptions.
La bibliothèque standard C++ le fait implicitement pour toutes les fonctions de la bibliothèque standard C.
Les fonctions constexpr peuvent lancer lorsqu’elles sont évaluées à l’exécution, donc vous pourriez avoir besoin de noexcept conditionnel pour certaines d’elles.
Vous pouvez utiliser noexcept même sur des fonctions qui peuvent lancer :
vector<string> collect(istream& is) noexcept
{
vector<string> res;
for (string s; is >> s;)
res.push_back(s);
return res;
}
Si collect() s’épuise de mémoire, le programme plante.
À moins que le programme ne soit conçu pour survivre à l’épuisement de mémoire, ce pourrait être la bonne chose à faire ;
terminate() pourrait produire une trace de logs d’erreur (mais après l’épuisement de mémoire, il est difficile de faire quelque chose de malin).
Vous devez tenir compte de l’environnement d’exécution dans lequel votre code est exécuté lorsqu’on décide d’étiqueter une fonction noexcept, surtout à cause du problème des exceptions et de l’allocation.
Le code général (comme la bibliothèque standard et d’autres utilitaires de ce genre) doit supporter les environnements où une exception bad_alloc peut être levée.
Cependant, la plupart des programmes et environnements n’ont pas de mécanisme pour gérer une perte d’allocation, et aborter le programme est la réponse la plus propre et la plus simple.
S’il est sûr que votre code d’application ne peut pas répondre à une exception d’allocation, vous pourriez envisager de +noexcept même sur des fonctions qui allouent.
Un autre point : noexcept est le plus utile (et le plus clairement correct) pour les fonctions bas niveau très utilisées.
Les destructeurs, les fonctions swap, les opérations de déplacement et les constructeurs par défaut ne doivent jamais lancer.
Il faut faire vigilance sur les fonctions virtuelles de base et les fonctions faites partie d’une interface publique, puisque déclarer une fonction noexcept impose une garantie que toutes les implémentations actuelles et futures doivent respecter.
Pour une fonction virtuelle, tous les remplacants doivent être noexcept aussi, et retirer noexcept d’une fonction pourrait casser les fonctions appelantes.
noexcept.swap, les opérations de déplacement, les destructeurs et les constructeurs par défaut qui lèvent éventuellement.T* ou T& plutôt que des pointeurs intelligentsPasser un pointeur intelligent transfère ou partage la possession et ne doit être utilisé que lorsqu’on veut une signification de possession.
Une fonction qui ne manipule pas la durée de vie doit prendre des pointeurs bruts ou des références à la place.
Passer par un pointeur intelligent limite l’utilisation d’une fonction aux appelants qui utilisent des pointeurs intelligents.
Une fonction qui a besoin d’un widget doit pouvoir accepter n’importe quel widget, pas seulement ceux dont la durée de vie est gérée par un type de pointeur intelligent particulier.
Passer un shared_ptr (par ex., std::shared_ptr) implique un coût d’exécution à l’échelle du temps de calcul.
// accepte n’importe quel int*
void f(int*);
// ne peut accepter que des ints pour lesquels vous souhaitez transférer la possession
void g(unique_ptr<int>);
// ne peut accepter que des ints pour lesquels vous êtes prêt à partager la possession
void g(shared_ptr<int>);
// ne change pas la possession, mais exige une possession particulière de l’appelant
void h(const unique_ptr<int>&);
// accepte n’importe quel int
void h(int&);
// appelant
void f(shared_ptr<widget>& w)
{
// ...
use(*w); // seule utilisation de w -- la durée de vie n’est utilisée du tout
// ...
};
// appel
shared_ptr<widget> my_widget = /* ... */;
f(my_widget);
widget stack_widget;
f(stack_widget); // erreur
// appelant
void f(widget& w)
{
// ...
use(w);
// ...
};
// appel
shared_ptr<widget> my_widget = /* ... */;
f(*my_widget);
widget stack_widget;
f(stack_widget); // ok -- cela fonctionne maintenant
On peut attraper beaucoup de cas courants d’explosions de pointeurs circulant statiquement (voir le profil de sécurité de durée de vie).
Les arguments de fonction vivent naturellement pour la durée d’appel et ont donc moins de problèmes de durée de vie.
operator*, operator-> ou get().T* ou T& à la place.operator-> ou operator*) copiable/mouvable mais qui n’est jamais copié/mouvoir à partir de la fonction, jamais modifié et ne passe pas à une autre fonction qui pourrait le faire. Cela signifie que les caractéristiques de possession ne sont pas utilisées.T* ou T& à la place.Voir aussi :
T* sur T& lorsque « pas d’argument » est une option valideLes fonctions pures sont plus faciles à raisonner, parfois plus faciles à optimiser (et même à paralléliser), et parfois peuvent être mémoïzées.
template<class T>
auto square(T t) { return t * t; }
Pas possible.
Lisibilité. Suppression d’avertissements sur les paramètres inutilisés.
widget* find(const set<widget>& s, const widget& w, Hint); // un jour, un indice était utilisé
Autoriser les paramètres sans nom a été introduit dans les années 1980 pour résoudre ce problème.
Si les paramètres sont conditionnellement inutilisés, les déclarer avec l’attribut [[maybe_unused]].
Par ex. :
template <typename Value>
Value* find(const set<Value>& s, const Value& v, [[maybe_unused]] Hint h)
{
if constexpr (sizeof(Value) > CacheSize)
{
// un indice est utilisé uniquement si Value a une certaine taille
}
}
Avertir les paramètres non utilisés nommés.
Documentation, lisibilité, opportunité de réutilisation.
struct Rec {
string name;
string addr;
int id; // identifiant unique
};
bool same(const Rec& a, const Rec& b)
{
return a.id == b.id;
}
vector<Rec*> find_id(const string& name); // trouver tous les enregistrements pour "name"
…
(Le reste de cette section est traduit de façon identique, tout en conservant la mise en page et les commentaires dans le code.)
Cela rend le code concis et donne une meilleure localisation que les alternatives.
…
(La partie restante suit le même schéma.)
…
(Le texte complet est traduit fidèlement dans le même format.)
(Le reste du document est traduit de façon identique en respectant la mise en page, les indentations de 4 espaces pour les exemples de code C++ et sans utiliser les blocs ` ```cpp `.)