Funzioni Inline e Funzioni constexpr in C++

Concetti Chiave
  • Le funzioni inline e le funzioni constexpr in C++ sono meccanismi che ottimizzano le prestazioni e permettono calcoli a tempo di compilazione rispettivamente.
  • Le funzioni inline riducono l'overhead della chiamata di funzione espandendo il corpo della funzione direttamente nel punto di chiamata.
  • Le funzioni constexpr possono essere valutate a tempo di compilazione se i loro argomenti sono espressioni costanti.
  • Le funzioni inline e constexpr possono essere definite multiple volte nel programma, ma tutte le definizioni devono essere identiche.

Funzioni inline

Consideriamo la seguente funzione:

// trova la più corta di due stringhe
const string &
stringaPiuCorta(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

Si tratta di una funzione semplice e diretta che restituisce un riferimento alla più corta delle due stringhe passate come argomenti. Ci si potrebbe chiedere: Perché definire una tale funzione invece di usare direttamente l'espressione condizionale che essa incapsula?

I benefici di definire una funzione per una tale piccola operazione includono i seguenti:

  • È più facile leggere e capire una chiamata a stringaPiuCorta di quanto sarebbe leggere e capire l'espressione condizionale equivalente.
  • Usare una funzione assicura un comportamento uniforme. Ogni test è garantito essere fatto nello stesso modo.
  • Se dobbiamo cambiare il calcolo, è più facile cambiare la funzione che trovare e cambiare ogni occorrenza dell'espressione equivalente.
  • La funzione può essere riusata piuttosto che riscritta per altre applicazioni.

C'è, tuttavia, un potenziale svantaggio nel fare di stringaPiuCorta una funzione: Chiamare una funzione è probabile che sia più lento che valutare l'espressione equivalente.

Sulla maggior parte delle macchine, una chiamata di funzione, in realtà, si traduce in una serie di operazioni a livello di codice macchina: i registri del processore sono salvati prima della chiamata e ripristinati dopo l'istruzione return; gli argomenti possono essere copiati; e il programma salta a una nuova locazione. Tutte queste operazioni richiedono tempo di esecuzione. Per una funzione piccola come stringaPiuCorta, l'overhead della chiamata di funzione può essere significativamente più grande del tempo necessario per eseguire il corpo della funzione stessa.

Per questo motivo, il C++ fornisce un meccanismo per ridurre l'overhead della chiamata di funzione per funzioni piccole e semplici: le funzioni inline. Tale meccanismo è stato, poi, riportato anche nel linguaggio C a partire dallo standard C99.

Le Funzioni inline Evitano l'Overhead della Chiamata di Funzione

Una funzione specificata come inline (di solito) è espansa "in linea" ad ogni chiamata.

In altri termine, ogni volta che il programma chiama una funzione inline, il compilatore sostituisce la chiamata con il corpo della funzione stessa evitando così l'overhead della chiamata di funzione.

Ritorniamo all'esempio di stringaPiuCorta. Supponiamo di avere il seguente codice che chiama stringaPiuCorta:

string s1 = "Ciao";
string s2 = "Salve";
cout << stringaPiuCorta(s1, s2) << endl;

Se stringaPiuCorta fosse definita come inline, allora questa chiamata (probabilmente) sarebbe espansa durante la compilazione in qualcosa come:

string s1 = "Ciao";
string s2 = "Salve";
cout << (s1.size() < s2.size() ? s1 : s2) << endl;

L'overhead run-time di rendere stringaPiuCorta una funzione è quindi rimosso.

Possiamo definire stringaPiuCorta come una funzione inline mettendo la parola chiave inline prima del tipo di ritorno della funzione:

// versione inline: trova la più corta di due stringhe
inline const string &
stringaPiuCorta(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

Tuttavia, la specifica inline è solo una richiesta al compilatore. Il compilatore potrebbe scegliere di ignorare questa richiesta.

In generale, il meccanismo inline è destinato ad ottimizzare funzioni piccole, semplici ed entro-contenute che sono chiamate frequentemente. Molti compilatori non trasformeranno una funzione ricorsiva in una funzione inline. Una funzione di 300 righe di codice quasi certamente non sarà espansa inline.

Funzioni constexpr

Una funzione constexpr è una funzione che può essere usata in un'espressione costante. Una funzione constexpr è definita come qualsiasi altra funzione ma deve soddisfare certe restrizioni:

  1. Il tipo di ritorno e il tipo di ogni parametro in una deve essere un tipo letterale, ossia un tipo che può essere espresso da un'espressione letterale.
  2. Il corpo della funzione deve contenere esattamente un'istruzione return.

Ecco un esempio di una funzione constexpr:

constexpr int nuova_dim() { return 42; }

// OK: foo è un'espressione costante
constexpr int foo = nuova_dim();

Qui abbiamo definito nuova_dim come una constexpr che non prende argomenti. Il compilatore può verificare a tempo di compilazione che una chiamata a nuova_dim restituisce un'espressione costante, quindi possiamo usare nuova_dim per inizializzare la nostra variabile constexpr, foo.

Quando può farlo, il compilatore sostituirà una chiamata a una funzione constexpr con il suo valore risultante. Per essere in grado di espandere la funzione immediatamente, le funzioni constexpr sono implicitamente inline.

Un corpo di funzione constexpr può contenere altre istruzioni finché quelle istruzioni non generano azioni a tempo di esecuzione. Ad esempio, una funzione constexpr può contenere istruzioni nulle, alias di tipo, e dichiarazioni using.

Una funzione constexpr può restituire un valore che non è una costante:

// scala(arg) è un'espressione costante se arg è un'espressione costante
constexpr size_t scala(size_t cont) { return nuova_dim() * cont; }

La funzione scala restituirà un'espressione costante se il suo argomento è un'espressione costante ma non altrimenti:

int arr[scala(2)]; // ok: scala(2) è un'espressione costante
int i = 2; // i non è un'espressione costante
int a2[scala(i)]; // ERRORE: scala(i) non è un'espressione costante

Quando passiamo un'espressione costante, come ad esempio il letterale 2, allora il return è un'espressione costante. In questo caso, il compilatore sostituirà la chiamata a scala con il valore risultante.

Se chiamiamo scala con un'espressione che non è un'espressione costante, come sull'oggetto int i, allora il return non è un'espressione costante. Se usiamo scala in un contesto che richiede un'espressione costante, il compilatore verifica che il risultato sia un'espressione costante. Se non lo è, il compilatore produrrà un messaggio di errore.

Una funzione constexpr, quindi, non deve garantire la restituizione di un'espressione costante.

A differenza di altre funzioni, le funzioni inline e constexpr possono essere definite multiple volte nel programma. Dopotutto, il compilatore ha bisogno della definizione, non solo della dichiarazione, per espandere il codice. Tuttavia, tutte le definizioni di una data inline o constexpr devono corrispondere esattamente. Come risultato, le funzioni inline e constexpr normalmente sono definite negli header.

Esercizi

  • Quale una delle seguenti dichiarazioni e definizioni metteresti in un header? In un file sorgente? Spiega perché.

    inline bool eq(const BigInt&, const BigInt&) { /* ... */ };
    void putValues(int \*arr, int size);
    

    Risposta: La dichiarazione e definizione di eq dovrebbe essere messa in un header perché è una funzione inline e il compilatore ha bisogno della sua definizione per poterla espandere inline. La dichiarazione di putValues dovrebbe essere messa in un header, mentre la sua definizione dovrebbe essere messa in un file sorgente perché non è una funzione inline e non è necessario che il compilatore abbia accesso alla sua definizione in ogni unità di traduzione.

  • Scrivi una funzione inline che prenda due argomenti di tipo const string & e restituisca true se la prima stringa è più corta della seconda, false altrimenti. Chiama la funzione isShorter.

    Risposta:

    inline bool isShorter(const string &s1, const string &s2) {
        return s1.size() < s2.size();
    }
    
  • Sarebbe possibile definire isShorter come una constexpr? Se sì, fallo. Se no, spiega perché no.

    Risposta: No, non sarebbe possibile definire isShorter come una constexpr per due motivi:

    1. Il tipo string non è un tipo letterale, quindi non può essere usato come tipo di parametro o di ritorno in una funzione constexpr.
    2. La funzione isShorter non può essere valutata a tempo di compilazione perché la lunghezza di una stringa può variare a runtime.