Czy poleganie na niejawnej konwersji argumentów jest uważane za niebezpieczne?

10

C ++ ma funkcję (nie potrafię ustalić jej właściwej nazwy), która automatycznie wywołuje pasujące konstruktory typów parametrów, jeśli typy argumentów nie są oczekiwanymi.

To bardzo prosty przykład jest to wywołanie funkcji, która spodziewa się std::stringz const char*argumentem. Kompilator automatycznie wygeneruje kod w celu wywołania odpowiedniego std::stringkonstruktora.

Zastanawiam się, czy to jest tak złe dla czytelności, jak mi się wydaje?

Oto przykład:

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

Czy to w porządku? Czy to idzie za daleko? Jeśli nie powinienem tego zrobić, czy mogę w jakiś sposób zmusić Clanga lub GCC do ostrzeżenia?

futlib
źródło
1
co jeśli Draw zostanie później przeładowany wersją łańcuchową?
maniak zapadkowy
1
według odpowiedzi @Dave'a Ragera, nie sądzę, że to się skompiluje na wszystkich kompilatorach. Zobacz mój komentarz do jego odpowiedzi. Najwyraźniej zgodnie ze standardem c ++ nie można łączyć takich ukrytych konwersji. Możesz dokonać tylko jednej konwersji i nie więcej.
Jonathan Henson
OK przepraszam, nie skompilowałem tego. Zaktualizowałem przykład i nadal jest okropny, IMO.
futlib

Odpowiedzi:

24

Nazywa się to konstruktorem konwertującym (lub czasem niejawnym konstruktorem lub niejawną konwersją).

Nie znam przełącznika czasu kompilacji, który ostrzega, kiedy to nastąpi, ale bardzo łatwo jest temu zapobiec; wystarczy użyć explicitsłowa kluczowego.

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

To, czy konwersja konstruktorów jest dobrym pomysłem: zależy.

Okoliczności, w których domniemana konwersja ma sens:

  • Klasa jest na tyle tania, że ​​można ją skonstruować tak, aby nie obchodziło ją, czy jest skonstruowana w sposób dorozumiany.
  • Niektóre klasy są pojęciowo podobne do swoich argumentów (takie jak std::stringodzwierciedlenie tego samego pojęcia, z const char *którego można niejawnie przekonwertować), więc konwersja niejawna ma sens.
  • Niektóre klasy stają się o wiele bardziej nieprzyjemne w użyciu, jeśli niejawna konwersja jest wyłączona. (Pomyśl o konieczności jawnego wywoływania std :: string za każdym razem, gdy chcesz przekazać literał ciągu. Części wzmocnienia są podobne.)

Okoliczności, w których domniemana konwersja ma mniej sensu:

  • Budowa jest droga (na przykład przykładowa tekstura, która wymaga załadowania i parsowania pliku graficznego).
  • Klasy są koncepcyjnie bardzo odmienne od swoich argumentów. Rozważmy na przykład kontener podobny do tablicy, który przyjmuje swój argument jako argument:
    klasa FlagList
    {
        FlagList (int rozmiar_ początkowy); 
    };

    void SetFlags (const FlagList & flag_list);

    int main () {
        // Teraz to się kompiluje, nawet jeśli wcale nie jest to oczywiste
        // co robi.
        SetFlags (42);
    }
  • Budowa może mieć niepożądane skutki uboczne. Na przykład AnsiStringklasa nie powinna niejawnie konstruować z a UnicodeString, ponieważ konwersja Unicode na ANSI może spowodować utratę informacji.

Dalsza lektura:

Josh Kelley
źródło
3

To jest bardziej komentarz niż odpowiedź, ale zbyt duży, aby można go było wstawić.

Co ciekawe, g++nie pozwala mi tego zrobić:

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

Wytwarza następujące:

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

Jeśli jednak zmienię wiersz na:

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

Dokonuje tej konwersji.

Dave Rager
źródło
To rzeczywiście interesująca „funkcja” w g ++. Przypuszczam, że jest to albo błąd, który sprawdza tylko jeden typ głębokości zamiast zejść rekurencyjnie w miarę możliwości w czasie kompilacji w celu wygenerowania poprawnego kodu, lub istnieje flaga, którą należy ustawić w poleceniu g ++.
Jonathan Henson
1
en.cppreference.com/w/cpp/language/implicit_cast wydaje się, że g ++ bardzo ściśle przestrzega tego standardu. Jest to kompilator Microsoft lub Mac, który jest zbyt hojny w stosunku do kodu OP. Szczególnie wymowne jest stwierdzenie: „Rozważając argument skierowany do konstruktora lub funkcji konwersji zdefiniowanej przez użytkownika, dozwolona jest tylko jedna standardowa sekwencja konwersji (w przeciwnym razie konwersje zdefiniowane przez użytkownika mogłyby zostać skutecznie powiązane).”
Jonathan Henson
Tak, właśnie rzuciłem kod razem, aby przetestować niektóre gccopcje kompilatora (które nie wyglądają na takie, które mogłyby rozwiązać ten konkretny przypadek). Nie zagłębiałem się w to (powinienem pracować :-), ale biorąc pod uwagę gccprzestrzeganie standardu i użycie explicitsłowa kluczowego opcja kompilatora prawdopodobnie została uznana za niepotrzebną.
Dave Rager
Niejawne konwersje nie są powiązane i Textureprawdopodobnie nie należy ich budować w sposób dorozumiany (zgodnie z wytycznymi w innych odpowiedziach), więc byłaby lepsza strona wywołująca renderer.Draw(Texture("foo.png"));(zakładając, że działa zgodnie z oczekiwaniami).
Blaisorblade,
3

Nazywa się to konwersją typu niejawnego. Ogólnie rzecz biorąc, jest to dobra rzecz, ponieważ hamuje niepotrzebne powtarzanie. Na przykład automatycznie otrzymujesz std::stringwersję Drawbez konieczności pisania dla niej dodatkowego kodu. Może również pomóc w przestrzeganiu zasady otwartego zamknięcia, ponieważ pozwala rozszerzyć Renderermożliwości bez modyfikacji Renderer.

Z drugiej strony nie jest to pozbawione wad. Może z jednej strony utrudnić ustalenie, skąd pochodzi argument. Czasami może przynieść nieoczekiwane wyniki w innych przypadkach. Po to explicitjest słowo kluczowe. Jeśli umieścisz go w Texturekonstruktorze, spowoduje to wyłączenie tego konstruktora do niejawnej konwersji typu. Nie znam metody globalnego ostrzegania przed niejawną konwersją typów, ale to nie znaczy, że metoda nie istnieje, tylko że gcc ma niezrozumiale dużą liczbę opcji.

Karl Bielefeldt
źródło