¿Es peligroso confiar en la conversión implícita de argumentos?

10

C ++ tiene una característica (no puedo encontrar el nombre propio), que llama automáticamente a los constructores coincidentes de los tipos de parámetros si los tipos de argumento no son los esperados.

Un ejemplo muy básico de esto es llamar a una función que espera a std::stringcon un const char*argumento. El compilador generará automáticamente código para invocar al std::stringconstructor apropiado .

Me pregunto, ¿es tan malo para la legibilidad como creo que es?

Aquí hay un ejemplo:

class Texture {
public:
    Texture(const std::string& imageFile);
};

class Renderer {
public:
    void Draw(const Texture& texture);
};

Renderer renderer;
std::string path = "foo.png";
renderer.Draw(path);

¿Eso está bien? ¿O va demasiado lejos? Si no debería hacerlo, ¿puedo hacer que Clang o GCC lo adviertan de alguna manera?

futlib
fuente
1
¿Qué pasa si Draw fue sobrecargado con una versión de cadena más tarde?
monstruo de trinquete
1
Según la respuesta de @Dave Rager, no creo que esto se compile en todos los compiladores. Vea mi comentario sobre su respuesta. Aparentemente, de acuerdo con el estándar c ++, no puede encadenar conversiones implícitas como esta. Solo puedes hacer una conversión y no más.
Jonathan Henson
OK lo siento, en realidad no compilé esto. Se actualizó el ejemplo y sigue siendo horrible, en mi opinión.
futlib

Respuestas:

24

Esto se conoce como un constructor de conversión (o, a veces, constructor implícito o conversión implícita).

No conozco un cambio de tiempo de compilación para advertir cuando esto ocurre, pero es muy fácil de prevenir; solo usa la explicitpalabra clave.

class Texture {
public:
    explicit Texture(const std::string& imageFile);
};

En cuanto a si los constructores de conversión son o no una buena idea: depende.

Circunstancias en las que la conversión implícita tiene sentido:

  • La clase es lo suficientemente barata para construir que no te importa si está implícitamente construida.
  • Algunas clases son conceptualmente similares a sus argumentos (como std::stringreflejar el mismo concepto del que const char *se puede convertir implícitamente), por lo que la conversión implícita tiene sentido.
  • Algunas clases se vuelven mucho más desagradables de usar si la conversión implícita está deshabilitada. (Piense en tener que invocar explícitamente std :: string cada vez que desee pasar un literal de cadena. Partes de Boost son similares).

Circunstancias en las que la conversión implícita tiene menos sentido:

  • La construcción es costosa (como el ejemplo de Texture, que requiere cargar y analizar un archivo gráfico).
  • Las clases son conceptualmente muy diferentes a sus argumentos. Considere, por ejemplo, un contenedor tipo matriz que toma su tamaño como argumento:
    clase FlagList
    {
        FlagList (int initial_size); 
    };

    anular SetFlags (const FlagList & flag_list);

    int main () {
        // Ahora esto se compila, aunque no es del todo obvio
        // lo que está haciendo.
        SetFlags (42);
    }
  • La construcción puede tener efectos secundarios no deseados. Por ejemplo, una AnsiStringclase no debe construir implícitamente a partir de a UnicodeString, ya que la conversión de Unicode a ANSI puede perder información.

Otras lecturas:

Josh Kelley
fuente
3

Esto es más un comentario que una respuesta, pero es demasiado grande para ponerlo en un comentario.

Curiosamente, g++no me deja hacer eso:

#include <iostream>
#include <string>

class Texture {
        public:
                Texture(const std::string& imageFile)
                {
                        std::cout << "Texture()" << std::endl;
                }
};

class Renderer {
        public:
                void Draw(const Texture& texture)
                {
                        std::cout << "Renderer.Draw()" << std::endl;
                }
};

int main(int argc, char* argv[])
{
        Renderer renderer;
        renderer.Draw("foo.png");

        return 0;
}

Produce lo siguiente:

$ g++ -o Conversion.exe Conversion.cpp 
Conversion.cpp: In function int main(int, char**)’:
Conversion.cpp:23:25: error: no matching function for call to Renderer::Draw(const char [8])’
Conversion.cpp:14:8: note: candidate is: void Renderer::Draw(const Texture&)

Sin embargo, si cambio la línea a:

   renderer.Draw(std::string("foo.png"));

Realizará esa conversión.

Dave Rager
fuente
Esa es una "característica" interesante en g ++ de hecho. Supongo que es un error que solo verifica un tipo de profundidad en lugar de ir recursivamente hacia abajo tanto como sea posible en el momento de la compilación para generar el código correcto, o hay un indicador que debe establecerse en su comando g ++.
Jonathan Henson
1
en.cppreference.com/w/cpp/language/implicit_cast parece que g ++ sigue estrictamente el estándar. Es el compilador de Microsoft o Mac el que está siendo demasiado generoso con el código del OP. Especialmente reveladora es la afirmación: "Cuando se considera el argumento para un constructor o una función de conversión definida por el usuario, solo se permite una secuencia de conversión estándar (de lo contrario, las conversiones definidas por el usuario podrían encadenarse efectivamente)".
Jonathan Henson
Sí, acabo de unir el código para probar algunas de las gccopciones del compilador (que no parece que haya ninguna para abordar este caso en particular). No busqué mucho más (se supone que debo estar trabajando :-), pero dada gccla adhesión al estándar y el uso de la explicitpalabra clave, una opción de compilador probablemente se consideró innecesaria.
Dave Rager
Las conversiones implícitas no están encadenadas, y Textureprobablemente no debería construirse implícitamente (de acuerdo con las pautas en otras respuestas), por lo que sería un mejor sitio de llamada renderer.Draw(Texture("foo.png"));(suponiendo que funcione como espero).
Blaisorblade
3

Se llama conversión de tipo implícito. En general es algo bueno, ya que inhibe la repetición innecesaria. Por ejemplo, obtiene automáticamente una std::stringversión de Drawsin tener que escribir ningún código adicional para ello. También puede ayudar a seguir el principio abierto-cerrado, ya que le permite ampliar Rendererlas capacidades sin modificarse Renderer.

Por otro lado, no está exento de inconvenientes. Por un lado, puede dificultar averiguar de dónde proviene un argumento. A veces puede producir resultados inesperados en otros casos. Para eso está la explicitpalabra clave. Si lo coloca en el Textureconstructor, deshabilita el uso de ese constructor para la conversión de tipo implícito. No conozco un método para advertir globalmente sobre la conversión de tipo implícita, pero eso no significa que no exista un método, solo que gcc tiene una cantidad incomprensiblemente grande de opciones.

Karl Bielefeldt
fuente