Wird es als gefährlich angesehen, sich auf die implizite Argumentkonvertierung zu verlassen?

10

C ++ hat eine Funktion (ich kann den richtigen Namen nicht herausfinden), die automatisch übereinstimmende Konstruktoren von Parametertypen aufruft, wenn die Argumenttypen nicht die erwarteten sind.

Ein sehr einfaches Beispiel hierfür ist das Aufrufen einer Funktion, die a std::stringmit einem const char*Argument erwartet . Der Compiler generiert automatisch Code, um den entsprechenden std::stringKonstruktor aufzurufen .

Ich frage mich, ist es so schlecht für die Lesbarkeit, wie ich denke?

Hier ist ein Beispiel:

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

Ist das in Ordnung? Oder geht es zu weit? Wenn ich es nicht tun sollte, kann ich Clang oder GCC irgendwie davor warnen lassen?

Futlib
quelle
1
Was ist, wenn Draw später mit einer String-Version überladen wurde?
Ratschenfreak
1
Gemäß der Antwort von @ Dave Rager denke ich nicht, dass dies auf allen Compilern kompiliert werden kann. Siehe meinen Kommentar zu seiner Antwort. Anscheinend können Sie gemäß dem c ++ - Standard implizite Konvertierungen wie diese nicht verketten. Sie können nur eine Konvertierung durchführen und nicht mehr.
Jonathan Henson
OK, sorry, habe das nicht kompiliert. Das Beispiel wurde aktualisiert und es ist immer noch schrecklich, IMO.
Futlib

Antworten:

24

Dies wird als konvertierender Konstruktor (oder manchmal impliziter Konstruktor oder implizite Konvertierung) bezeichnet.

Mir ist kein Wechsel zur Kompilierungszeit bekannt, der warnt, wenn dies auftritt, aber es ist sehr einfach zu verhindern. Verwenden Sie einfach das explicitSchlüsselwort.

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

Ob das Konvertieren von Konstruktoren eine gute Idee ist oder nicht: Es kommt darauf an.

Umstände, unter denen implizite Konvertierung Sinn macht:

  • Die Klasse ist billig genug, um zu konstruieren, dass es Ihnen egal ist, ob sie implizit konstruiert ist.
  • Einige Klassen ähneln konzeptionell ihren Argumenten (z. B. std::stringspiegeln sie dasselbe Konzept const char *wider, aus dem sie implizit konvertieren können), sodass eine implizite Konvertierung sinnvoll ist.
  • Einige Klassen sind viel unangenehmer zu verwenden, wenn die implizite Konvertierung deaktiviert ist. (Denken Sie daran, std :: string jedes Mal explizit aufrufen zu müssen, wenn Sie ein String-Literal übergeben möchten. Teile von Boost sind ähnlich.)

Umstände, unter denen implizite Konvertierung weniger sinnvoll ist:

  • Die Konstruktion ist teuer (z. B. Ihr Texturbeispiel, bei dem eine Grafikdatei geladen und analysiert werden muss).
  • Klassen sind konzeptionell ihren Argumenten sehr unähnlich. Stellen Sie sich zum Beispiel einen Array-ähnlichen Container vor, der seine Größe als Argument verwendet:
    Klasse FlagList
    {
        FlagList (int initial_size); 
    };

    void SetFlags (const FlagList & flag_list);

    int main () {
        // Jetzt wird dies kompiliert, obwohl es überhaupt nicht offensichtlich ist
        // was es tut.
        SetFlags (42);
    }}
  • Konstruktion kann unerwünschte Nebenwirkungen haben. Beispielsweise sollte eine AnsiStringKlasse nicht implizit aus a konstruieren UnicodeString, da bei der Konvertierung von Unicode in ANSI möglicherweise Informationen verloren gehen.

Weiterführende Literatur:

Josh Kelley
quelle
3

Dies ist eher ein Kommentar als eine Antwort, aber zu groß, um einen Kommentar abzugeben.

Interessanterweise g++lässt mich das nicht tun:

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

Produziert folgendes:

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

Wenn ich jedoch die Zeile ändere in:

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

Diese Konvertierung wird durchgeführt.

Dave Rager
quelle
Das ist in der Tat eine interessante "Funktion" in g ++. Ich nehme an, das ist entweder ein Fehler, der nur einen Typ tief prüft, anstatt zur Kompilierungszeit so weit wie möglich rekursiv nach unten zu gehen, um den richtigen Code zu generieren, oder es gibt ein Flag, das in Ihrem g ++ - Befehl gesetzt werden muss.
Jonathan Henson
1
en.cppreference.com/w/cpp/language/implicit_cast Es scheint, dass g ++ dem Standard ziemlich streng folgt. Es ist der Compiler von Microsoft oder Mac, der mit dem OP-Code etwas zu großzügig umgeht. Besonders aussagekräftig ist die Aussage: "Wenn man das Argument für einen Konstruktor oder eine benutzerdefinierte Konvertierungsfunktion betrachtet, ist nur eine Standardkonvertierungssequenz zulässig (andernfalls könnten benutzerdefinierte Konvertierungen effektiv verkettet werden)."
Jonathan Henson
Ja, ich habe den Code einfach zusammengeschmissen, um einige der gccCompileroptionen zu testen (die anscheinend nicht für diesen speziellen Fall geeignet sind). Ich habe mich nicht weiter damit befasst (ich sollte arbeiten :-), aber angesichts gccder Einhaltung des Standards und der Verwendung des explicitSchlüsselworts wurde eine Compileroption wahrscheinlich als unnötig erachtet.
Dave Rager
Implizite Conversions sind nicht verkettet und a Texturesollte wahrscheinlich nicht implizit erstellt werden (gemäß den Richtlinien in anderen Antworten), sodass eine bessere Call-Site wäre renderer.Draw(Texture("foo.png"));(vorausgesetzt, sie funktioniert wie erwartet).
Blaisorblade
3

Es heißt implizite Typkonvertierung. Im Allgemeinen ist es eine gute Sache, da es unnötige Wiederholungen verhindert. Beispielsweise erhalten Sie automatisch eine std::stringVersion von, Drawohne zusätzlichen Code dafür schreiben zu müssen. Es kann auch dabei helfen, dem Open-Closed-Prinzip zu folgen, da Sie damit die RendererFunktionen erweitern können, ohne sich Rendererselbst zu ändern .

Auf der anderen Seite ist es nicht ohne Nachteile. Zum einen kann es schwierig sein, herauszufinden, woher ein Argument kommt. In anderen Fällen kann es manchmal zu unerwarteten Ergebnissen kommen. Dafür steht das explicitSchlüsselwort. Wenn Sie es auf den TextureKonstruktor setzen, wird die Verwendung dieses Konstruktors für die implizite Typkonvertierung deaktiviert. Mir ist keine Methode bekannt, mit der bei impliziter Typkonvertierung global gewarnt werden kann. Dies bedeutet jedoch nicht, dass keine Methode vorhanden ist, sondern nur, dass gcc über eine unverständlich große Anzahl von Optionen verfügt.

Karl Bielefeldt
quelle