Domanda

Sto scrivendo una libreria che vorrei essere portatile. Pertanto, non dovrebbe dipendere da estensioni di glibc o Microsoft o da qualsiasi altra cosa che non sia nello standard. Ho una bella gerarchia di classi derivate da std :: exception che uso per gestire gli errori nella logica e nell'input. Sapere che un particolare tipo di eccezione è stato generato in un determinato file e il numero di riga è utile, ma sapere come è arrivata l'esecuzione sarebbe potenzialmente molto più prezioso, quindi ho cercato modi per acquisire la traccia dello stack.

Sono consapevole che questi dati sono disponibili quando si crea contro glibc usando le funzioni in execinfo.h (vedere domanda 76822 ) e tramite l'interfaccia StackWalk nell'implementazione C ++ di Microsoft (vedere domanda 126450 ), ma mi piacerebbe molto evitare qualsiasi cosa che non sia portatile.

Stavo pensando di implementare questa funzionalità da solo in questa forma:

class myException : public std::exception
{
public:
  ...
  void AddCall( std::string s )
  { m_vCallStack.push_back( s ); }
  std::string ToStr() const
  {
    std::string l_sRet = "";
    ...
    l_sRet += "Call stack:\n";
    for( int i = 0; i < m_vCallStack.size(); i++ )
      l_sRet += "  " + m_vCallStack[i] + "\n";
    ...
    return l_sRet;
  }
private:
  ...
  std::vector< std::string > m_vCallStack;
};

ret_type some_function( param_1, param_2, param_3 )
{
  try
  {
    ...
  }
  catch( myException e )
  {
    e.AddCall( "some_function( " + param_1 + ", " + param_2 + ", " + param_3 + " )" );
    throw e;
  }
}

int main( int argc, char * argv[] )
{
  try
  {
    ...
  }
  catch ( myException e )
  {
    std::cerr << "Caught exception: \n" << e.ToStr();
    return 1;
  }
  return 0;
}

È un'idea terribile? Significherebbe molto lavoro aggiungendo blocchi try / catch ad ogni funzione, ma posso conviverci. Non funzionerebbe quando la causa dell'eccezione è il danneggiamento della memoria o la mancanza di memoria, ma a quel punto sei praticamente fregato comunque. Potrebbe fornire informazioni fuorvianti se alcune funzioni nello stack non rilevano eccezioni, si aggiungono all'elenco e si ricodificano, ma posso almeno fornire una garanzia che tutte le funzioni della mia libreria lo fanno. A differenza di un "reale" stack trace Non otterrò il numero di riga nelle funzioni di chiamata, ma almeno avrei qualcosa.

La mia preoccupazione principale è la possibilità che ciò provochi un rallentamento anche quando non vengono effettivamente emesse eccezioni. Tutti questi blocchi try / catch richiedono un'impostazione e uno smontaggio aggiuntivi su ogni invocazione della funzione o vengono in qualche modo gestiti al momento della compilazione? O ci sono altri problemi che non ho preso in considerazione?

È stato utile?

Soluzione

Penso che sia una pessima idea.

La portabilità è un obiettivo molto meritevole, ma non quando si traduce in una soluzione invadente, che peggiora le prestazioni e un'implementazione inferiore.

Ogni piattaforma (Windows / Linux / PS2 / iPhone / ecc.) su cui ho lavorato ha offerto un modo per camminare nello stack quando si verifica un'eccezione e abbinare gli indirizzi ai nomi delle funzioni. Sì, nessuno di questi è portatile ma può esserlo il framework di reportistica e di solito ci vuole meno di un giorno o due per scrivere una versione specifica del codice di stack walking.

Non solo è meno tempo di quanto ci vorrebbe per creare / mantenere una soluzione multipiattaforma, ma i risultati sono molto migliori;

  • Non è necessario modificare le funzioni
  • Blocca gli arresti anomali nelle librerie standard o di terze parti
  • Non c'è bisogno di provare / catturare in tutte le funzioni (lente e ad alta memoria)

Altri suggerimenti

Cerca Contesto diagnostico nidificato una volta. Ecco un piccolo suggerimento:

class NDC {
public:
    static NDC* getContextForCurrentThread();
    int addEntry(char const* file, unsigned lineNo);
    void removeEntry(int key);
    void dump(std::ostream& os);
    void clear();
};

class Scope {
public:
    Scope(char const *file, unsigned lineNo) {
       NDC *ctx = NDC::getContextForCurrentThread();
       myKey = ctx->addEntry(file,lineNo);
    }
    ~Scope() {
       if (!std::uncaught_exception()) {
           NDC *ctx = NDC::getContextForCurrentThread();
           ctx->removeEntry(myKey);
       }
    }
private:
    int myKey;
};
#define DECLARE_NDC() Scope s__(__FILE__,__LINE__)

void f() {
    DECLARE_NDC(); // always declare the scope
    // only use try/catch when you want to handle an exception
    // and dump the stack
    try {
       // do stuff in here
    } catch (...) {
       NDC* ctx = NDC::getContextForCurrentThread();
       ctx->dump(std::cerr);
       ctx->clear();
    }
}

Il sovraccarico è nell'implementazione del NDC. Stavo giocando con una versione valutata pigramente e con una versione che conteneva solo un numero fisso di voci. Il punto chiave è che se usi costruttori e distruttori per gestire lo stack in modo da non aver bisogno di tutti quei cattivi blocchi prova / catch e manipolazione esplicita ovunque.

L'unico mal di testa specifico della piattaforma è il metodo getContextForCurrentThread () . È possibile utilizzare un'implementazione specifica della piattaforma utilizzando l'archiviazione locale dei thread per gestire il lavoro nella maggior parte, se non in tutti i casi.

Se sei più orientato alle prestazioni e vivi nel mondo dei file di registro, cambia l'ambito per tenere un puntatore al nome del file e al numero di riga e ometti del tutto l'NDC:

class Scope {
public:
    Scope(char const* f, unsigned l): fileName(f), lineNo(l) {}
    ~Scope() {
        if (std::uncaught_exception()) {
            log_error("%s(%u): stack unwind due to exception\n",
                      fileName, lineNo);
        }
    }
private:
    char const* fileName;
    unsigned lineNo;
};

Questo ti darà una bella traccia dello stack nel tuo file di registro quando viene generata un'eccezione. Non c'è bisogno di camminare per uno stack reale, solo un piccolo messaggio di registro quando viene generata un'eccezione;)

Non credo che ci sia una "quotazione indipendente dalla piattaforma" modo per farlo - dopo tutto, se ci fosse, non ci sarebbe bisogno di StackWalk o delle speciali funzionalità di tracciamento dello stack gcc che menzionate.

Sarebbe un po 'disordinato, ma il modo in cui lo implementerei sarebbe quello di creare una classe che offre un'interfaccia coerente per accedere alla traccia dello stack, quindi avere #ifdefs nell'implementazione che utilizza i metodi specifici specifici della piattaforma per effettivamente mettere insieme la traccia dello stack.

In questo modo il tuo utilizzo della classe è indipendente dalla piattaforma e solo quella classe dovrebbe essere modificata se desideri scegliere come target un'altra piattaforma.

Nel debugger:

Per ottenere la traccia dello stack da dove viene generata un'eccezione, ho appena individuato il punto di interruzione nel costruttore std :: exception.

Pertanto, quando viene creata l'eccezione, il debugger si interrompe e in quel momento è possibile vedere la traccia dello stack. Non perfetto ma funziona il più delle volte.

La gestione dello stack è una di quelle cose semplici che si complicano molto rapidamente. Meglio lasciarlo per biblioteche specializzate. Hai provato libunwind? Funziona alla grande e AFAIK è portatile, anche se non l'ho mai provato su Windows.

Questo sarà più lento ma sembra che dovrebbe funzionare.

Da quello che ho capito il problema nel creare una traccia dello stack veloce e portatile è che l'implementazione dello stack è sia specifica per OS che per CPU, quindi è implicitamente un problema specifico della piattaforma. Un'alternativa sarebbe quella di utilizzare le funzioni MS / glibc e di usare #ifdef e le definizioni del preprocessore appropriate (ad esempio _WIN32) per implementare le soluzioni specifiche della piattaforma in build diverse.

Poiché l'utilizzo dello stack dipende fortemente dalla piattaforma e dall'implementazione, non è possibile farlo direttamente in modo completamente portatile. Tuttavia, è possibile creare un'interfaccia portatile per una piattaforma e un'implementazione specifica del compilatore, localizzando i problemi il più possibile. IMHO, questo sarebbe il tuo approccio migliore.

L'implementazione del tracciante verrebbe quindi collegata a qualsiasi libreria di helper specifica per la piattaforma disponibile. Funzionerebbe quindi solo quando si verifica un'eccezione, e anche solo se lo si chiamasse da un blocco catch. La sua API minima restituirebbe semplicemente una stringa contenente l'intera traccia.

Richiedere al programmatore di iniettare l'elaborazione catch and rethrow nella catena di chiamate comporta costi di runtime significativi su alcune piattaforme e impone un grande costo di manutenzione futuro.

Detto questo, se scegli di utilizzare il meccanismo catch / throw, non dimenticare che anche C ++ ha ancora il preprocessore C disponibile e che le macro __FILE__ e __LINE __ sono definiti. È possibile utilizzarli per includere il nome del file di origine e il numero di riga nelle informazioni di traccia.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top