Domanda Come fa il compilatore a trarre vantaggio dalla nuova parola chiave finale di C ++?


C ++ 11 consentirà di contrassegnare le classi e il metodo virtuale finale proibire di derivarne o di annullarli.

class Driver {
  virtual void print() const;
};
class KeyboardDriver : public Driver {
  void print(int) const final;
};
class MouseDriver final : public Driver {
  void print(int) const;
};
class Data final {
  int values_;
};

Questo è molto utile, perché dice al lettore dell'interfaccia qualcosa sull'intento dell'uso di questa classe / metodo. Potrebbe essere utile anche l'utente che ottiene la diagnostica se tenta di eseguire l'override.

Ma c'è un vantaggio dal punto di vista dei compilatori? Il compilatore può fare qualcosa di diverso quando sa "questa classe non sarà mai derivata da" o "questa funzione virtuale non sarà mai sovrascritta"?

Per final Ho trovato principalmente solo N2751 che si riferiva ad esso. Spulciando tra alcune discussioni ho trovato argomenti provenienti dal lato C ++ / CLI, ma nessun chiaro suggerimento sul perché final può essere utile per il compilatore. Ci sto pensando, perché vedo anche alcuni svantaggi nel segnare una lezione final: Per eseguire il test unitario delle funzioni membro membro è possibile derivare una classe e inserire il codice di prova. A volte queste classi sono buoni candidati per essere contrassegnati con final. Questa tecnica sarebbe impossibile in questi casi.


29
2017-09-24 11:50


origine


risposte:


Posso pensare a uno scenario in cui potrebbe essere utile al compilatore da un'ottica di ottimizzazione. Non sono sicuro che valga la pena di implementare i compilatori, ma è teoricamente possibile almeno.

Con virtual chiamare l'invio su un derivato, final scrivi puoi essere sicuro che non c'è nient'altro derivante da quel tipo. Ciò significa che (almeno in teoria) il final parola chiave consentirebbe di risolvere correttamente alcuni virtual chiama al momento della compilazione, il che renderebbe possibile una serie di ottimizzazioni altrimenti impossibili virtual chiamate.

Ad esempio, se lo hai delete most_derived_ptr, dove most_derived_ptr è un puntatore a un derivato, final Digitare quindi è possibile per il compilatore per semplificare le chiamate al virtual distruttore.

Allo stesso modo per le chiamate a virtual membro funzioni su riferimenti / puntatori al tipo più derivato.

Sarei molto sorpreso se alcuni compilatori lo facessero oggi, ma sembra il tipo di cosa che potrebbe essere implementata nel prossimo decennio.

Potrebbe anche esserci qualche millesimo nel riuscire a dedurne (in assenza di friends) cose contrassegnate protected in un final  class anche efficacemente diventare private.


36
2017-09-24 12:02



Le chiamate virtuali alle funzioni sono leggermente più costose delle normali chiamate. Oltre a eseguire effettivamente la chiamata, il runtime deve prima determinare quale funzione chiamare, che spesso porta a:

  1. Individuazione del puntatore v-table, e attraverso esso raggiunge la v-table
  2. Individuazione del puntatore della funzione all'interno della v-table e attraverso di essa esecuzione della chiamata

Rispetto ad una chiamata diretta in cui l'indirizzo della funzione è noto in anticipo (e codificato con un simbolo), questo porta a un piccolo overhead. I buoni compilatori riescono a farlo solo del 10% -15% più lento di una normale chiamata, che di solito è insignificante se la funzione ha carne.

L'ottimizzatore di un compilatore cerca ancora di evitare tutti i tipi di spese generali, e devirtualizing le chiamate di funzione sono generalmente un frutto a basso impatto. Ad esempio, vedere in C ++ 03:

struct Base { virtual ~Base(); };

struct Derived: Base { virtual ~Derived(); };

void foo() {
  Derived d; (void)d;
}

Clang ottiene:

define void @foo()() {
  ; Allocate and initialize `d`
  %d = alloca i8**, align 8
  %tmpcast = bitcast i8*** %d to %struct.Derived*
  store i8** getelementptr inbounds ([4 x i8*]* @vtable for Derived, i64 0, i64 2), i8*** %d, align 8

  ; Call `d`'s destructor
  call void @Derived::~Derived()(%struct.Derived* %tmpcast)

  ret void
}

Come puoi vedere, il compilatore era già abbastanza intelligente per determinarlo d essere un Derived quindi non è necessario sostenere l'overhead della chiamata virtuale.

In effetti, ottimizzerebbe la seguente funzione altrettanto bene:

void bar() {
  Base* b = new Derived();
  delete b;
}

Tuttavia ci sono alcune situazioni in cui il compilatore non può raggiungere questa conclusione:

Derived* newDerived();

void deleteDerived(Derived* d) { delete d; }

Qui potremmo aspettarci (ingenuamente) che una chiamata a deleteDerived(newDerived()); risulterebbe nello stesso codice di prima. Tuttavia non è il caso:

define void @foobar()() {
  %1 = tail call %struct.Derived* @newDerived()()
  %2 = icmp eq %struct.Derived* %1, null
  br i1 %2, label %_Z13deleteDerivedP7Derived.exit, label %3

; <label>:3                                       ; preds = %0
  %4 = bitcast %struct.Derived* %1 to void (%struct.Derived*)***
  %5 = load void (%struct.Derived*)*** %4, align 8
  %6 = getelementptr inbounds void (%struct.Derived*)** %5, i64 1
  %7 = load void (%struct.Derived*)** %6, align 8
  tail call void %7(%struct.Derived* %1)
  br label %_Z13deleteDerivedP7Derived.exit

_Z13deleteDerivedP7Derived.exit:                  ; preds = %3, %0
  ret void
}

La convenzione potrebbe darglielo newDerived restituisce a Derived, ma il compilatore non può fare una tale ipotesi: e se restituisse qualcosa di ulteriore derivato? E così puoi vedere tutti i brutti meccanismi coinvolti nel recupero del puntatore v-table, selezionare la voce appropriata nella tabella e infine eseguire la chiamata.

Se comunque mettiamo a final in, quindi diamo al compilatore una garanzia che non può essere nient'altro:

define void @deleteDerived2(Derived2*)(%struct.Derived2* %d) {
  %1 = icmp eq %struct.Derived2* %d, null
  br i1 %1, label %4, label %2

; <label>:2                                       ; preds = %0
  %3 = bitcast i8* %1 to %struct.Derived2*
  tail call void @Derived2::~Derived2()(%struct.Derived2* %3)
  br label %4

; <label>:4                                      ; preds = %2, %0
  ret void
}

In breve: final consente al compilatore di evitare il sovraccarico delle chiamate virtuali per le funzioni interessate in situazioni in cui è impossibile rilevarlo.


28
2017-09-24 13:08



A seconda di come la si guarda, c'è un ulteriore vantaggio per il compilatore (anche se questo beneficio si riduce semplicemente all'utente, quindi questo non è un vantaggio del compilatore): il compilatore può evitare di emettere avvisi per i costrutti con un comportamento incerto. essere superabile

Ad esempio, considera questo codice:

class Base
{
  public:
    virtual void foo() { }
    Base() { }
    ~Base();
};

void destroy(Base* b)
{
  delete b;
}

Molti compilatori emetteranno un avvertimento per bè il distruttore non virtuale quando il delete b è osservato. Se una classe Derived ereditato da Base e aveva il suo ~Derived distruttore, usando destroy su un allocazione dinamica Derived l'istanza sarebbe di solito chiamata (per comportamento specifico non definito) ~Base, ma non chiamerebbe ~Derived. così ~DerivedLe operazioni di pulizia non sarebbero avvenute, e ciò potrebbe essere negativo (sebbene probabilmente non catastrofico, nella maggior parte dei casi).

Se il compilatore lo sa Base non può essere ereditato da, tuttavia, quindi non è un problema ~Base non è virtuale, poiché non è possibile saltare accidentalmente alcuna pulizia derivata. Aggiunta final a class Base fornisce al compilatore le informazioni per non emettere un avviso.

So per certo che usando final in questo modo sopprimerà un avvertimento con Clang. Non so se altri compilatori emettono un avviso qui, o se prendono in considerazione la finalità nel determinare se emettere o meno un avviso.


0
2017-11-20 07:58