Controllo della compilazione per NULL inizializzato std :: string

7

Questa è una sorta di domanda complementare a Come proteggere al meglio da 0 passati a parametri std :: string? . Fondamentalmente, sto cercando di capire se c'è un modo per far sì che il compilatore mi avvisi se un percorso di codice proverebbe incondizionatamente a chiamare std::string di char* costruttore con NULL .

I controlli del tempo di esecuzione sono tutti buoni e positivi, ma per un caso come:

std::string get_first(const std::string& foo) {
    if (foo.empty()) return 0; // Or NULL, or nullptr
    return foo.substr(0, 1);
}

è fastidioso, anche se il codice è garantito fallire se si esercita quel percorso di codice, e l'intestazione di sistema è solitamente annotata con la precondizione che dice che il puntatore non deve essere nullo, questo ancora passa la compilazione sotto gcc , clang , ecc., anche con -std=c++11 -Wall -Wextra -pedantic -Werror . Posso bloccare il caso specifico di 0 su gcc con -Werror=zero-as-null-pointer-constant , ma ciò non aiuta con NULL / nullptr ed è una specie di affrontare il problema correlato ma dissimile. Il problema principale è che un programmatore può commettere questo errore con 0 , NULL o nullptr e non notarlo se il percorso del codice non viene esercitato.

È possibile forzare questo controllo a compilare il tempo, coprendo un'intera base di codice, senza sciocchezze come sostituire std::string con una speciale sottoclasse in tutto il codice?

    
posta ShadowRanger 11.02.2016 - 18:34
fonte

2 risposte

2

Ci sono diversi approcci, a seconda che funzioni su tutti i compilatori, in circostanze molto limitate e con alcuni effetti collaterali, o solo su alcuni, ma in cambio molto più in generale:

  1. Aggiunta di sovraccarichi che si lamentano quando vengono utilizzati. Esiste [[deprecated]] da C ++ 11 che si lamenterà nel sito di chiamata immediato, a condizione che in quanto non è soppresso, come normalmente in un'intestazione di sistema.

    GCC e CLANG forniscono un attributo personalizzato più adatto, __attribute__((error("message"))) , che interromperà sempre la build se viene utilizzata la funzione e il nome del sito di chiamata.

    Il problema con l'aggiunta di sovraccarichi che accettano tutte quelle cose che potrebbero essere nullpointer-letterali, è che potrebbe confondere la SFINAE di altri modelli, rompendo così il codice, e non può prendere un argomento già di tipo char* che sfortunatamente capita di essere un nullpointer:

    // added overload, common lines:
    template<class X, class = typename
        std::enable_if<std::is_arithmetic<X>() && !std::is_pointer<X>()>
    // best on GCC / clang:
    string(X) __attribute__((error("nullpointer")));
    // best on others:
    [[deprecated("UB")]] string(X) /* no definition, link breaks */;
    
  2. L'alternativa preferibile è contrassegnare l'argomento come non nullo e lasciare che l'ottimizzatore del compilatore lo calcoli. Stai chiedendo e ascoltando tali avvertimenti, giusto?
    Questa è solo un'opzione in GCC e CLANG, ma evita lo svantaggio di sovraccarichi aggiuntivi e cattura tutti i casi che il compilatore può capire, il che significa che funziona meglio con più ottimizzazione.

    basic_string(const CharT* s,
                 size_type count,
                 const Allocator& alloc = Allocator()) __attribute__((nonnull));
    basic_string(const CharT* s,
                 const Allocator& alloc = Allocator()) __attribute__((nonnull));
    

    Generalmente, si può chiedere a GCC / clang se può determinare il valore di qualsiasi espressione in fase di compilazione, usando __builtin_constant_p .

risposta data 12.02.2016 - 04:53
fonte
1

Non sarebbe fatto in fase di compilazione, ma sarebbe abbastanza facile definire un tipo di stringa che gestisse questo con più garbo di (almeno la maggior parte delle implementazioni di) std::string attualmente.

La base sarebbe il fatto che NULL ha tipo int e nullptr ha tipo nullptr_t . In quanto tale, sarebbe abbastanza facile sovraccaricare su questi tipi e farli fallire "veloce e rumoroso" quando vengono passati i tipi errati di argomenti:

#include <iostream>
#include <fstream>
#include <algorithm>
#include <iterator>
#include <vector>
#include <numeric>
#include <string>

class test_string {
    std::vector<char> data;
public:

    test_string(nullptr_t) { throw std::runtime_error("Bad input data"); }
    test_string(int) { throw std::runtime_error("Bad input data"); }
    test_string(void *) { throw std::runtime_error("Bad input data"); }
    test_string(char const *s) { size_t length = strlen(s); data.resize(length); std::copy_n(s, length, &data[0]); }
    size_t length() const { return data.size(); }

    friend std::ostream &operator<<(std::ostream &os, test_string const &s) {
        std::copy(s.data.begin(), s.data.end(), std::ostream_iterator<char>(os));
        return os;
    }
};

int main() {

    // These will all immediately throw if uncommmented:
    //test_string x(NULL);
    //test_string y(nullptr);
    //test_string z(0);

    // But this still works fine:
    test_string a("This is a string");
    std::cout << a;
}

Fare questo con std::string potrebbe essere un po 'più difficile. Il problema è che std::string ha già alcuni sovraccarichi del suo costruttore. Il sovraccarico di nullptr_t dovrebbe essere sempre sicuro (l'unica cosa di quel tipo è nullptr , e chiaramente non lo vuoi mai).

Avere un sovraccarico per prendere int al posto di ogni charT const * potrebbe essere più difficile. Il problema è che alcuni dei sovraccarichi già distinguono l'intento in base (per esempio) alla posizione del puntatore rispetto a un conteggio che indica la lunghezza. Dovresti guardare con molta attenzione per essere sicuro che stavi catturando l'invocazione con NULL, ma senza interferire con un uso (legittimo) esistente che richiede (per esempio) un char e un size_t . Non sono convinto che sia impossibile, ma ci vorrebbe un po 'di cura (e potresti finire per dover vivere lasciando alcuni casi che potrebbero ancora passare).

Per essere molto pratico, tuttavia, probabilmente questa dovrebbe essere una modifica a std::string - averla come una classe separata porta ad una qualche impraticabilità abbastanza seria (almeno secondo me).

    
risposta data 12.02.2016 - 02:26
fonte

Leggi altre domande sui tag