Le recours à une conversion implicite d'arguments est-il dangereux?

10

C ++ a une fonctionnalité (je ne peux pas en trouver le nom correct), qui appelle automatiquement les constructeurs correspondants des types de paramètres si les types d'arguments ne sont pas ceux attendus.

Un exemple très basique de ceci appelle une fonction qui attend un std::stringavec un const char*argument. Le compilateur générera automatiquement du code pour appeler le std::stringconstructeur approprié .

Je me demande, est-ce aussi mauvais pour la lisibilité que je le pense?

Voici un exemple:

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);

C'est bien? Ou va-t-il trop loin? Si je ne devais pas le faire, puis-je faire en sorte que Clang ou GCC en avertisse?

futlib
la source
1
que faire si Draw a été surchargé avec une version chaîne plus tard?
monstre à cliquet
1
selon la réponse de @Dave Rager, je ne pense pas que cela se compilera sur tous les compilateurs. Voir mon commentaire sur sa réponse. Apparemment, selon la norme c ++, vous ne pouvez pas chaîner des conversions implicites comme celle-ci. Vous ne pouvez effectuer qu'une seule conversion et pas plus.
Jonathan Henson
OK désolé, je n'ai pas compilé cela. Mis à jour l'exemple et c'est toujours horrible, OMI.
futlib

Réponses:

24

Ceci est appelé constructeur de conversion (ou parfois constructeur implicite ou conversion implicite).

Je ne suis pas au courant d'un commutateur de compilation pour avertir lorsque cela se produit, mais c'est très facile à empêcher; utilisez simplement le explicitmot - clé.

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

Quant à savoir si la conversion des constructeurs est une bonne idée: cela dépend.

Circonstances dans lesquelles la conversion implicite est logique:

  • La classe est suffisamment bon marché pour être construite que vous vous en fichez si elle est implicitement construite.
  • Certaines classes sont conceptuellement similaires à leurs arguments (comme std::stringrefléter le même concept que const char *celui à partir duquel elles peuvent implicitement se convertir), donc la conversion implicite est logique.
  • Certaines classes deviennent beaucoup plus désagréables à utiliser si la conversion implicite est désactivée. (Pensez à devoir invoquer explicitement std :: string chaque fois que vous voulez passer un littéral de chaîne. Certaines parties de Boost sont similaires.)

Circonstances dans lesquelles la conversion implicite est moins logique:

  • La construction est coûteuse (comme votre exemple de texture, qui nécessite le chargement et l'analyse d'un fichier graphique).
  • Les classes sont conceptuellement très différentes de leurs arguments. Prenons, par exemple, un conteneur de type tableau qui prend sa taille en argument:
    classe FlagList
    {
        FlagList (int initial_size); 
    };

    void SetFlags (const FlagList & flag_list);

    int main() {
        // Maintenant, cela compile, même si ce n'est pas du tout évident
        // ce qu'il fait.
        SetFlags (42);
    }
  • La construction peut avoir des effets secondaires indésirables. Par exemple, une AnsiStringclasse ne doit pas implicitement construire à partir de a UnicodeString, car la conversion Unicode en ANSI peut perdre des informations.

Lectures complémentaires:

Josh Kelley
la source
3

Il s'agit plus d'un commentaire que d'une réponse, mais trop gros pour mettre un commentaire.

Fait intéressant, g++ne me laisse pas faire cela:

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

Produit les éléments suivants:

$ 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&)

Cependant, si je change la ligne en:

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

Il effectuera cette conversion.

Dave Rager
la source
C'est vraiment une "fonctionnalité" intéressante dans g ++. Je suppose que c'est soit un bogue qui vérifie uniquement un type en profondeur au lieu de descendre récursivement autant que possible au moment de la compilation pour générer le code correct, soit un indicateur doit être défini dans votre commande g ++.
Jonathan Henson
1
en.cppreference.com/w/cpp/language/implicit_cast il semble que g ++ suive la norme assez strictement. C'est le compilateur de Microsoft ou Mac qui est un peu trop généreux avec le code de l'OP. La déclaration est particulièrement révélatrice: "Lorsque l'on considère l'argument à un constructeur ou à une fonction de conversion définie par l'utilisateur, une seule séquence de conversion standard est autorisée (sinon les conversions définies par l'utilisateur pourraient être enchaînées efficacement)."
Jonathan Henson
Ouais, je viens de jeter le code ensemble pour tester certaines des gccoptions du compilateur (qui ne semblent pas exister pour résoudre ce cas particulier). Je n'y ai pas beaucoup approfondi (je suis censé travailler :-) mais étant donné gccl'adhésion à la norme et l'utilisation du explicitmot clé, une option de compilation a probablement été jugée inutile.
Dave Rager
Les conversions implicites ne sont pas enchaînées, et un Texturene devrait probablement pas être construit implicitement (selon les directives dans d'autres réponses), donc un meilleur site d'appel serait renderer.Draw(Texture("foo.png"));(en supposant qu'il fonctionne comme je m'y attendais).
Blaisorblade
3

C'est ce qu'on appelle la conversion de type implicite. En général, c'est une bonne chose, car elle empêche la répétition inutile. Par exemple, vous obtenez automatiquement une std::stringversion de Drawsans avoir à écrire de code supplémentaire pour celle-ci. Il peut également aider à suivre le principe ouvert-fermé, car il vous permet d'étendre Rendererles capacités de sans se modifier Renderer.

D'un autre côté, ce n'est pas sans inconvénients. Il peut être difficile de comprendre d'où vient un argument, d'une part. Il peut parfois produire des résultats inattendus dans d'autres cas. C'est à ça que explicitsert le mot-clé. Si vous le placez sur le Textureconstructeur, il désactive l'utilisation de ce constructeur pour la conversion de type implicite. Je ne suis pas au courant d'une méthode pour avertir globalement de la conversion de type implicite, mais cela ne signifie pas qu'une méthode n'existe pas, mais seulement que gcc a un nombre incompréhensiblement grand d'options.

Karl Bielefeldt
la source