2015-02-28 6 views
21

Sto cercando di compilare il codice seguente semplice C++ usando Clang-3.5:clang: no out-of-line definizioni virtuali metodo (puro estratto di classe C++)

test.h:

class A 
{ 
    public: 
    A(); 
    virtual ~A() = 0; 
}; 

test.cc:

#include "test.h" 

A::A() {;} 
A::~A() {;} 

Il comando che uso per la compilazione di questo (Linux, uname -r: 3.16.0-4-amd64):

$clang-3.5 -Weverything -std=c++11 -c test.cc 

E l'errore che ottengo:

./test.h:1:7: warning: 'A' has no out-of-line virtual method definitions; its vtable will be emitted in every translation unit [-Wweak-vtables] 

Eventuali suggerimenti perché questo sta emettendo un avvertimento? Il distruttore virtuale non è affatto delineato. Al contrario, c'è una definizione out-of-line fornita in test.cc. Cosa mi manca qui?

Modifica

Non credo che questa domanda è un duplicato di: What is the meaning of clang's -Wweak-vtables? come Filip Roséen suggerito. Nella mia domanda faccio specifico riferimento alle classi astratte pure (non menzionate nel duplicato suggerito). So come -Wweak-vtables funziona con classi non astratte e io sto bene con esso. Nel mio esempio definisco il distruttore (che è puro astratto) nel file di implementazione. Ciò dovrebbe impedire a Clang di emettere errori, anche con -Wweak-vtables.

+0

@ gha.st In effetti, ma che differenza non fa? Mi sono imbattuto in questo problema mentre implementavo una classe astratta più complessa, ma qui è irrilevante. Il codice sopra dovrebbe ancora andare bene, giusto? –

+0

Non è questo l'esatto duplicato di [quest'altra domanda] (http://stackoverflow.com/q/28788353/2069064) che hai chiesto 3 ore dopo? – Barry

+0

@Barry Penso che questa sia la migliore domanda dei due, quindi questa è la cosa che ho generico. L'altra domanda spiega che al momento di chiederlo, questo era chiuso come un dupe di un altro, ma in seguito fu riaperto. – Oktalist

risposta

8

Non vogliamo posizionare il vtable in ogni unità di traduzione. Quindi ci deve essere un certo ordine di unità di traduzione, così che possiamo dire allora, che poniamo il vtable nella "prima" unità di traduzione. Se questo ordine non è definito, emetteremo l'avviso.

La risposta è nello Itanium CXX ABI. Nella sezione sulle tabelle virtuali (5.2.3) si trova:

La tabella virtuale per una classe viene emessa nello stesso oggetto contenente la definizione della sua funzione chiave, ovvero la prima funzione virtuale non pura che è non in linea al punto della definizione della classe. Se non esiste una funzione chiave, viene emessa ovunque utilizzata. La tabella virtuale emessa include il gruppo di tabelle virtuali completo per la classe, eventuali nuove tabelle virtuali di costruzione richieste per i sottoprogetti e il VTT per la classe. Vengono emessi in un gruppo COMDAT, con il nome di una tabella virtuale storpiato come simbolo di identificazione. Si noti che se la funzione chiave non è dichiarata in linea nella definizione della classe, ma la sua definizione in seguito viene sempre dichiarata in linea, verrà emessa in ogni oggetto che contiene la definizione.
NOTA: In astratto, un distruttore virtuale puro potrebbe essere utilizzato come funzione chiave, poiché deve essere definito anche se è puro. Tuttavia, il comitato ABI non ha realizzato questo fatto fino a quando non è stata completata la specifica della funzione chiave; quindi un distruttore virtuale puro non può essere la funzione chiave.

La seconda sezione è la risposta alla tua domanda. Un distruttore virtuale puro non ha alcuna funzione chiave. Pertanto, non è chiaro dove posizionare il vtable e viene posizionato ovunque. Di conseguenza otteniamo l'avvertimento.

È persino possibile trovare questa spiegazione nello Clang source documentation.

Così appositamente per l'avvertimento: Si otterrà l'avviso quando tutte le funzioni virtuali appartengono ad una delle seguenti categorie:

  1. inline è specificato per A::x() nella definizione della classe.

    struct A { 
        inline virtual void x(); 
        virtual ~A() { 
        } 
    }; 
    void A::x() { 
    } 
    
  2. B :: x() è in linea nella definizione della classe.

    struct B { 
        virtual void x() { 
        } 
        virtual ~B() { 
        } 
    }; 
    
  3. C :: x() è puro virtuale

    struct C { 
        virtual void x() = 0; 
        virtual ~C() { 
        } 
    }; 
    
  4. (Belongs a 3.) Si dispone di un distruttore virtuale pura

    struct D { 
        virtual ~D() = 0; 
    }; 
    D::~D() { 
    } 
    

    In questo caso, l'ordinamento potrebbe essere definito, poiché il distruttore deve essere definito, tuttavia, per definizione, non esiste ancora un'unità di traduzione "prima".

Per tutti gli altri casi, la funzione chiave è il primo virtuale funzione che non si adatta a una di queste categorie, e vtable saranno collocati in unità di traduzione in cui è definita la funzione chiave.

3

Ho finito per implementare un distruttore virtuale banale, invece di lasciarlo puro virtuale.

Così, invece di

class A { 
public: 
    virtual ~A() = 0; 
}; 

Io uso

class A { 
public: 
    virtual ~A(); 
}; 

quindi implementare il distruttore banale in un file cpp:

A::~A() 
{} 

Questo pin efficacemente vtable al cpp file, invece di emetterlo in più unità di traduzione (oggetti), evitando con successo l'avviso -Wweak-vtables g.

+2

Il tuo suggerimento potrebbe essere semplificato come: ** non usare ** classi astratte pure e non riceverai l'avviso. Come si collega alla mia domanda? È chiaro che l'intenzione dell'autore è effettivamente ** utilizzare ** una classe astratta pura. –

+1

È ancora concettualmente una classe base astratta pura, ma senza l'effetto collaterale della duplicazione del vtable. –

+0

Questo è quello che ho detto nella prima frase. Sto fornendo una possibile soluzione. Ho fornito una descrizione più lunga di due possibili soluzioni nel ripubblicare questa domanda: http://stackoverflow.com/questions/28788353/clang-wweak-vtables-and-pure-abstract-class - dove ho incluso un esempio di disabilitazione del avvertimento se vuoi mantenerlo puro astratto. –

10

Per un momento, dimentichiamoci delle funzioni virtuali pure e proviamo a capire come il compilatore può evitare di emettere il vtable in tutte le unità di traduzione che includono la dichiarazione di una classe polimorfica.

Quando il compilatore vede la dichiarazione di una classe con funzioni virtuali, controlla se ci sono funzioni virtuali che sono solo dichiarate ma non definite all'interno della dichiarazione di classe. Se esiste esattamente una di queste funzioni, il compilatore sa per certo che deve essere definito da qualche parte (altrimenti il ​​programma non collegherà) ed emetterà il vtable solo nell'unità di traduzione che ospita la definizione di tale funzione. Se ci sono più funzioni di questo tipo, il compilatore ne sceglie uno che utilizza alcuni criteri di selezione deterministica e - per quanto riguarda la decisione su dove emettere il vtable - ignora gli altri. Il modo più semplice per selezionare una funzione virtuale rappresentativa unica è quella di prendere il primo dal set candidato, e questo è ciò che fa clang.

Quindi, la chiave per questa ottimizzazione è selezionare un metodo virtuale tale che il compilatore possa garantire che incontrerà una (singola) definizione di quel metodo in qualche unità di traduzione.

Ora, cosa succede se la dichiarazione di classe contiene pure funzioni virtuali? Un programmatore può fornire un'implementazione per una funzione virtuale pura ma (s) lui non è obbligato a! Pertanto le funzioni virtuali pure non appartengono all'elenco dei metodi virtuali candidati dai quali il compilatore può selezionare quello rappresentativo.

Ma c'è un'eccezione: un distruttore virtuale puro!

Un distruttore virtuale pura è un caso speciale:

  1. Una classe astratta non ha senso se non si ha intenzione di ricavare altre classi da esso.
  2. Un distruttore di sottoclasse chiama sempre il distruttore della classe base.
  3. Il distruttore di una classe derivante da una classe con un distruttore virtuale è automaticamente una funzione virtuale.
  4. Tutte le funzioni virtuali di tutte le classi, create dal programma, sono di solito collegate nell'eseguibile finale (incluse le funzioni virtuali che possono essere staticamente dimostrate come non utilizzate, anche se ciò richiederebbe un'analisi statica dell'intero programma).
  5. Pertanto un distruttore virtuale puro deve disporre di una definizione fornita dall'utente.

Quindi, l'avvertimento di clang nell'esempio della domanda non è concettualmente giustificato.

Tuttavia, dal punto di vista pratico l'importanza di tale esempio è minima, dal momento che un distruttore virtuale puro è raramente, se non del tutto, necessario. Non riesco a immaginare un caso più o meno realistico in cui un puro distruttore virtuale non sarà accompagnato da un'altra pura funzione virtuale. Ma in tale contesto la necessità della purezza del distruttore (virtuale) scompare completamente, poiché la classe diventa astratta a causa della presenza di altri metodi puramente virtuali.

+0

Vedere anche [pensieri di Herb Sutter] (http://www.gotw.ca/gotw/031.htm) su distruttori virtuali puri. L'unica situazione in cui posso immaginare che un distruttore virtuale puro sia utile come unico metodo virtuale è quando si desidera una raccolta di puntatori, usando 'dynamic_cast' per lavorare con i puntatori. –