Comprobación en tiempo de compilación para std inicializado NULL :: cadena

7

Esto es una especie de pregunta complementaria a ¿Cómo proteger mejor los 0 pasados a los parámetros std :: string? . Básicamente, estoy tratando de averiguar si hay una manera de que el compilador me avise si una ruta de código intentaría incondicionalmente llamar al constructor std::string char* usando NULL .

Las comprobaciones de tiempo de ejecución están muy bien, pero para un caso como:

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

es molesto que, a pesar de que se garantiza que el código fallará si esa ruta de acceso del código se ejerce, y el encabezado del sistema generalmente se anota con la condición previa que dice que el puntero no debe ser nulo, esto todavía pasa la compilación bajo gcc , clang , etc., incluso con -std=c++11 -Wall -Wextra -pedantic -Werror . Puedo bloquear el caso específico de 0 en gcc con -Werror=zero-as-null-pointer-constant , pero eso no ayuda con NULL / nullptr y es una forma de abordar el problema relacionado pero diferente. El problema principal es que un programador puede cometer este error con 0 , NULL o nullptr y no notarlo si no se ejerce la ruta del código.

¿Es posible forzar que esta verificación sea un tiempo de compilación, que cubra una base de código completa, sin tonterías como reemplazar std::string con una subclase especial en todo el código?

    
pregunta ShadowRanger 11.02.2016 - 18:34

2 respuestas

2

Hay diferentes enfoques, dependiendo de si debería funcionar en todos los compiladores, en circunstancias muy restringidas y con algunos efectos secundarios, o solo en algunos, pero a cambio de manera mucho más amplia:

  1. Agregar sobrecargas que se quejan cuando se usan. Hay [[deprecated]] desde C ++ 11 que se quejará en el sitio de la llamada inmediata, siempre que ya que no se suprime, como normalmente en un encabezado del sistema.

    GCC y CLANG proporcionan un atributo personalizado más adecuado, __attribute__((error("message"))) , que siempre interrumpirá la compilación si se utiliza la función y nombra el sitio de la llamada.

    El problema con la adición de sobrecargas que aceptan todas esas cosas que podrían ser nulo-literales-nulo, es que podría confundir el SFINAE de otra plantilla, rompiendo así el código, y no puede atrapar un argumento ya del tipo char* , que desafortunadamente solo es un puntero nulo:

    // 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. La alternativa preferible es marcar el argumento como no nulo, y dejar que el optimizador de los compiladores lo descubra. Estás pidiendo y prestando atención a tales advertencias, ¿verdad?
    Esa es solo una opción en GCC y CLANG, pero evita la desventaja de sobrecargas adicionales y captura todos los casos que el compilador puede resolver, lo que significa que funciona mejor con más optimización.

    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, uno puede preguntar a GCC / clang si puede determinar el valor de cualquier expresión en tiempo de compilación, usando __builtin_constant_p .

respondido por el Deduplicator 12.02.2016 - 04:53
1

No se haría en tiempo de compilación, pero sería bastante fácil definir un tipo de cadena que manejó esto con más gracia que (al menos la mayoría de las implementaciones de) std::string actualmente.

La base sería el hecho de que NULL tiene el tipo int y nullptr tiene el tipo nullptr_t . Como tal, sería bastante fácil sobrecargar estos tipos y hacer que fallen "rápido y fuerte" cuando se pasan los tipos de argumentos incorrectos:

#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;
}

Hacer esto con std::string puede ser un poco más difícil. El problema es que std::string ya tiene bastantes sobrecargas de su constructor. La sobrecarga para nullptr_t siempre debe ser segura (la única cosa de ese tipo es nullptr , y claramente nunca quieres eso).

Tener una sobrecarga para tomar int en lugar de cada charT const * podría ser más difícil. El problema es que algunas de las sobrecargas ya distinguen la intención según (por ejemplo) la posición del puntero en comparación con un conteo que indica la longitud. Tendría que mirar con mucho cuidado para asegurarse de que estaba captando la invocación con NULL, pero sin interferir con un uso existente (legítimo) que toma (por ejemplo) un char y un size_t . No estoy convencido de que sea imposible, pero tomaría un poco de cuidado (y podría terminar teniendo que dejar algunos casos que aún podrían pasar).

Sin embargo, para ser muy práctico, esto probablemente tendría que ser una modificación de std::string . Tenerlo como una clase separada conduce a una impracticabilidad bastante seria (al menos en mi opinión).

    
respondido por el Jerry Coffin 12.02.2016 - 02:26

Lea otras preguntas en las etiquetas