Fonctions Inline (C++)

Le mot clé inline suggère que le compilateur remplace le code dans la définition de fonction à la place de chaque appel à cette fonction.

En théorie, l'utilisation des fonctions inline permet accélérer l'exécution de votre programme, car celles-ci éliminent la surcharge associée aux appels de fonction. L’appel d’une fonction nécessite d’envoyer (push) l’adresse de retour sur la pile, d’envoyer (push) des arguments sur la pile, de passer au corps de la fonction, puis d’exécuter une instruction de retour une fois la fonction terminée. Ce processus est éliminé par la mise en ligne de la fonction. Le compilateur a également différentes opportunités d’optimiser les fonctions développées inline par rapport à celles qui ne le sont pas. Un compromis entre les fonctions inline est que la taille globale de votre programme peut augmenter.

La substitution de code inline est effectuée à la discrétion du compilateur. Par exemple, le compilateur n’inline pas une fonction si son adresse est prise ou si le compilateur décide qu’il est trop volumineux.

Mot inline clé et règle de définition unique (ODR)

La signification d’origine inline est un indicateur pour le compilateur de préférer l’expansion du code sur le site d’appel de fonction par rapport aux instructions d’appel de fonction. Cela reste l’une des significations de inline.

Toutefois, le inline mot clé a également des implications pour la règle de définition unique (ODR). Normalement, une fonction ne peut être définie qu’une seule fois sur toutes les unités de traduction. Lorsqu’une fonction est marquée inline, elle peut être définie dans plusieurs unités de traduction (généralement via un fichier d’en-tête) si toutes les définitions sont identiques. L’éditeur de liens sélectionne ensuite une définition et ignore les doublons plutôt que de signaler une erreur.

Cette double nature d’un inlineindicateur d’optimisation et d’un mécanisme ODR peut entraîner une confusion. L’aspect ODR est une nécessité pratique où le même en-tête (contenant une définition de fonction inline) peut être inclus dans plusieurs fichiers sources.

Fonctions incorporées implicitement

Certaines fonctions sont implicitement inline sans nécessiter le mot clé :

  • Fonctions définies au niveau de l’étendue de classe : une fonction définie dans le corps d’une déclaration de classe est implicitement une fonction inline. Cela permet aux petites fonctions d’accesseur et d’être définies directement dans les définitions de classes sans entraîner de surcharge d’appel de fonction, une priorité depuis les premiers jours de C++.

  • constexpr fonctions : les fonctions déclarées constexpr (introduites en C++11) sont implicitement inline. Étant donné que constexpr les fonctions sont généralement définies dans les fichiers d’en-tête pour permettre l’évaluation au moment de la compilation, elles doivent suivre les mêmes règles ODR que les fonctions inline.

  • consteval fonctions : les fonctions déclarées consteval (introduites en C++20) sont implicitement inline.

Variables inline (C++17)

C++17 a étendu le inline mot clé aux variables. Une inline variable peut être définie dans plusieurs unités de traduction, et comme les fonctions inline, l’éditeur de liens sélectionne une définition et ignore le reste.

Les variables inline sont utiles pour définir des constantes ou des membres de données statiques dans des fichiers d’en-tête :

// constants.h
inline constexpr double pi = 3.14159265358979323846;

struct MyClass
{
    static inline int instanceCount = 0;  // Can be defined in header
};

Avant C++17, ces variables nécessitaient une définition distincte dans un fichier source unique pour éviter les erreurs de l’éditeur de liens.

Exemple : fonctions membres de classe inline

Dans la déclaration de classe suivante, le Account constructeur est une fonction inline, car elle est définie dans le corps de la déclaration de classe. Les fonctions membres GetBalance, Deposit et Withdraw sont spécifiées inline dans leurs définitions. Le mot clé inline est facultatif dans les déclarations de fonction dans la déclaration de classe.

// account.h
class Account
{
public:
    Account(double initial_balance)
    {
        balance = initial_balance;
    }

    double GetBalance() const;
    double Deposit(double amount);
    double Withdraw(double amount);

private:
    double balance;
};

inline double Account::GetBalance() const
{
    return balance;
}

inline double Account::Deposit(double amount)
{
    balance += amount;
    return balance;
}

inline double Account::Withdraw(double amount)
{
    balance -= amount;
    return balance;
}

Remarque

Dans la déclaration de classe, les fonctions ont été déclarées sans mot clé inline. Le mot clé inline peut être spécifié dans la déclaration de classe. Le résultat est identique.

Une fonction membre inline donnée doit être déclarée de la même manière dans chaque unité de compilation. Il doit exister une seule définition d'une fonction inline.

Une fonction membre de classe correspond par défaut à une liaison externe sauf qu'une définition de cette fonction contient le spécificateur inline. L’exemple précédent montre que vous n’avez pas besoin de déclarer ces fonctions explicitement avec le spécificateur inline. L’utilisation inline dans la définition de fonction suggère au compilateur de le traiter comme une fonction inline. Toutefois, vous ne pouvez pas redéclarer une fonction comme inline après un appel à cette fonction.

inline, __inline et __forceinline

Les spécificateurs inline et __inline indiquent au compilateur d'insérer une copie du corps de la fonction dans chaque emplacement où la fonction est appelée.

L'insertion appelée expansion inline ou incorporation se produit uniquement si l'analyse des coûts-bénéfice du compilateur montre qu'il vaut la peine de la faire. L'expansion inline minimise la surcharge des appels de fonction au coût potentiel d'une plus grande taille de code.

Le __forceinline mot clé (ou [msvc::forceinline] attribut) remplace l’analyse des coûts-avantages et s’appuie plutôt sur le jugement du programmeur. Soyez prudent lorsque vous utilisez __forceinline. L'utilisation sans discernement de __forceinline peut entraîner un code plus volumineux avec seulement des gains de performance marginaux ou, dans certains cas, des pertes de performance (en raison de l’augmentation de la pagination d'un fichier exécutable plus volumineux).

Le compilateur traite les options d'expansion inline et les mots clés comme des suggestions. Rien ne garantit que les fonctions seront incorporées. Vous ne pouvez pas forcer le compilateur à incorporer une fonction particulière, même avec le mot clé __forceinline. Lorsque vous compilez avec /clr, le compilateur n'incorpore pas une fonction si des attributs de sécurité lui sont appliqués.

Pour la compatibilité avec les versions précédentes et _inline_forceinline sont des synonymes pour __inline et __forceinline (deux traits de soulignement principaux), respectivement, sauf si l’option /Za du compilateur (Désactiver les extensions de langage) est spécifiée.

Le mot clé inline indique au compilateur que l'expansion inline est recommandée. Toutefois, le compilateur peut l’ignorer. Deux cas où ce comportement peut se produire sont les suivants :

  • Fonctions récursives.
  • Fonctions référencées par l'intermédiaire d'un pointeur ailleurs dans l'unité de traduction.

Ces raisons peuvent interférer avec la mise en ligne, comme les autres, comme le détermine le compilateur. Ne dépendez pas du spécificateur inline pour provoquer la mise en ligne d’une fonction.

Au lieu de développer une fonction inline définie dans un fichier d’en-tête, le compilateur peut le créer en tant que fonction pouvant être appelée dans plusieurs unités de traduction. Le compilateur marque la fonction générée pour l’éditeur de liens afin d’empêcher les violations ODR (one-definition-rule).

Comme pour les fonctions normales, il n’existe aucun ordre défini pour l’évaluation des arguments dans une fonction inline. En fait, il peut être différent de l’ordre d’évaluation de l’argument lorsqu’il est passé à l’aide du protocole d’appel de fonction normal.

Utilisez l’option d’optimisation du compilateur /Ob pour influencer si l’extension de fonction inline se produit réellement.
/LTCG effectue l’incorporation entre modules, qu’il soit demandé dans le code source ou non.

Exemple 1

// inline_keyword1.cpp
// compile with: /c
inline int max(int a, int b)
{
    return a < b ? b : a;
}

Les fonctions membres d'une classe peuvent être déclarées inline en utilisant le mot clé inline ou en plaçant la définition de la fonction dans la définition de la classe.

Exemple 2

// inline_keyword2.cpp
// compile with: /EHsc /c
#include <iostream>

class MyClass
{
public:
    void print() { std::cout << i; }   // Implicitly inline

private:
    int i;
};

Spécifique à Microsoft

Le mot clé __inline est équivalent à inline.

Même avec __forceinline, le compilateur ne peut pas inliner une fonction si :

  • La fonction ou son appelant est compilé avec /Ob0 (option par défaut pour les versions debug).
  • La fonction et l'appelant utilisent différents types de gestion des exceptions (gestion des exceptions C++ dans l'un, gestion structurée des exceptions dans l'autre).
  • La fonction a une liste d'arguments variable.
  • La fonction utilise l'assembly inline, sauf si elle est compilée avec /Ox, /O1 ou /O2.
  • La fonction est récursive et n’a pas #pragma inline_recursion(on) défini. Avec le pragma, les fonctions récursives sont incorporées à une profondeur par défaut de 16 appels. Pour réduire la profondeur de mise en ligne, utilisez le pragma inline_depth.
  • La fonction est virtuelle et est appelée virtuellement. Les appels directs aux fonctions virtuelles peuvent être incorporés.
  • Le programme prend l'adresse de la fonction et l'appel est effectué via le pointeur vers la fonction. Les appels directs aux fonctions dont l'adresse est acceptée peuvent être incorporés.
  • La fonction est également marquée avec le modificateur naked__declspec.

Si le compilateur ne peut pas incorporer une fonction déclarée avec __forceinline, il génère un avertissement de niveau 1, sauf quand :

  • La fonction est compilée à l’aide de /Od ou /Ob0. Aucune mise en ligne n’est attendue dans ces cas.
  • La fonction est définie en externe, dans une bibliothèque incluse ou une autre unité de traduction, ou est une cible d’appel virtuel ou une cible d’appel indirecte. Le compilateur ne peut pas identifier le code non inclus qu’il ne trouve pas dans l’unité de traduction actuelle.

Les fonctions récursives peuvent être remplacées par du code inline à une profondeur spécifiée par le pragma inline_depth, jusqu’à un maximum de 16 appels. Au-delà de cette profondeur, les appels de fonction récursive sont traités comme des appels à une instance de la fonction. La profondeur à laquelle les fonctions récursives sont examinées par l’heuristique inline ne peut pas dépasser 16. Le pragma inline_recursion contrôle l'expansion inline d'une fonction en cours d'expansion. Pour des informations connexes, consultez l'option du compilateur (/Ob) Expansion des fonctions inline.

La norme C++ définit un ensemble commun d'attributs. Il permet également aux fournisseurs de compilateur de définir leurs propres attributs au sein d’un espace de noms spécifique au fournisseur (dans notre cas, msvc). Les attributs spécifiques à Microsoft suivants peuvent être utilisés pour contrôler le comportement d’incorporation :

Attributs spécifiques à Microsoft pour contrôler le comportement d’inlining

Caractéristique Meaning
[msvc::forceinline] A la même signification que __forceinline.
[msvc::forceinline_calls] Peut être placé sur ou avant une instruction ou un bloc pour provoquer l’heuristique inline à forcer tous les appels de cette instruction ou de ce bloc.
[msvc::flatten] Similaire à [[msvc::forceinline_calls]], mais force-inline de manière récursive tous les appels dans l’étendue qu’il est appliqué jusqu’à ce qu’aucun appel ne soit laissé.
[msvc::noinline] Lorsqu’elle est placée avant une déclaration de fonction, elle a la même signification que __declspec(noinline).
[msvc::noinline_calls] Peut être placé avant toute instruction ou bloc pour désactiver l’incorporation pour tous les appels dans l’étendue à laquelle elle est appliquée.

FIN de la section spécifique à Microsoft

Pour plus d'informations sur l'utilisation du spécificateur inline, consultez :

Quand utiliser les fonctions inline

Les fonctions inline sont mieux utilisées pour les petites fonctions, telles que celles qui fournissent l’accès aux membres de données. Les fonctions courtes sont sensibles à la surcharge des appels de fonction. Les fonctions longues passent proportionnellement moins de temps dans la séquence d'appel/retour et bénéficient moins de l'inlining.

Une classe Point peut être définie comme suit :

// when_to_use_inline_functions.cpp
// compile with: /c
class Point
{
public:
    // Define "accessor" functions
    // as reference types.
    unsigned& x();
    unsigned& y();

private:
    unsigned _x;
    unsigned _y;
};

inline unsigned& Point::x()
{
    return _x;
}

inline unsigned& Point::y()
{
    return _y;
}

Si la manipulation des coordonnées est une opération relativement courante dans un client de cette classe, la spécification des deux fonctions d’accesseur (x et y dans l’exemple précédent) comme inline stocke généralement la charge sur :

  • Appels de fonction (notamment le paramètre passe et place l'adresse de l'objet dans la pile)
  • Conservation du frame de pile de l'appelant
  • Nouvelle installation du frame de pile
  • Communication de la valeur de retour
  • Restauration de l’ancien cadre de pile
  • Retour

Fonctions inline vs. macros

Une macro comporte certaines choses en commun avec une fonction inline. Toute fois, il existe deux différences majeures. Prenons l’exemple suivant :

#include <iostream>

#define mult1(a, b) a * b
#define mult2(a, b) (a) * (b)
#define mult3(a, b) ((a) * (b))

inline int multiply(int a, int b)
{
    return a * b;
}

int main()
{
    std::cout << (48 / mult1(2 + 2, 3 + 3)) << std::endl; // outputs 33
    std::cout << (48 / mult2(2 + 2, 3 + 3)) << std::endl; // outputs 72
    std::cout << (48 / mult3(2 + 2, 3 + 3)) << std::endl; // outputs 2
    std::cout << (48 / multiply(2 + 2, 3 + 3)) << std::endl; // outputs 2

    std::cout << mult3(2, 2.2) << std::endl; // no warning
    std::cout << multiply(2, 2.2); // Warning C4244	'argument': conversion from 'double' to 'int', possible loss of data
}
33
72
2
2
4.4
4

Voici quelques-unes des différences entre la macro et la fonction inline :

  • Les macros sont toujours développées inline. Toutefois, une fonction inline n’est incluse que lorsque le compilateur détermine qu’il s’agit de la chose optimale à faire.
  • La macro peut entraîner un comportement inattendu, ce qui peut entraîner des bogues subtils. Par exemple, l’expression mult1(2 + 2, 3 + 3) s’étend sur 2 + 2 * 3 + 3 laquelle la valeur est 11, mais le résultat attendu est 24. Un correctif apparemment valide consiste à ajouter des parenthèses autour des deux arguments de la macro de fonction, ce #define mult2(a, b) (a) * (b)qui entraîne , qui résout le problème à la main, mais peut encore provoquer un comportement surprenant lorsqu’une partie d’une expression plus grande. Cela a été démontré dans l’exemple précédent, et le problème peut être résolu en définissant la macro comme telle #define mult3(a, b) ((a) * (b)).
  • Une fonction inline est soumise au traitement sémantique par le compilateur, tandis que le préprocesseur développe des macros sans ce même avantage. Les macros ne sont pas de type sécurisé, tandis que les fonctions sont.
  • Les expressions passées comme arguments aux fonctions inline sont évaluées une fois. Dans certains cas, les expressions passées comme arguments aux macros peuvent être évaluées plusieurs fois. Prenons l’exemple suivant :
#include <iostream>

#define sqr(a) ((a) * (a))

int increment(int& number)
{
    return number++;
}

inline int square(int a)
{
    return a * a;
}

int main()
{
    int c = 5;
    std::cout << sqr(increment(c)) << std::endl; // outputs 30
    std::cout << c << std::endl; // outputs 7

    c = 5;
    std::cout << square(increment(c)) << std::endl; // outputs 25
    std::cout << c; // outputs 6
}
30
7
25
6

Dans cet exemple, la fonction increment est appelée deux fois au fur et à mesure que l’expression sqr(increment(c)) se développe sur ((increment(c)) * (increment(c))). Cela a fait que le deuxième appel de increment a retourné 6, d’où l’expression est évaluée à 30. Toute expression qui contient des effets secondaires peut affecter le résultat lorsqu’elle est utilisée dans une macro, examinez la macro entièrement développée pour vérifier si le comportement est prévu. Au lieu de cela, si la fonction inline square a été utilisée, la fonction increment ne serait appelée qu’une seule fois et le résultat correct de 25 sera obtenu.

Voir aussi

noinline
auto_inline
[msvc::forceinline]
[msvc::forceinline_calls]
[msvc::flatten]
[msvc::nolinline]
[msvc::nolinline_calls]