Опасно ли полагаться на неявное преобразование аргументов?

10

В C ++ есть особенность (я не могу определить ее правильное имя), которая автоматически вызывает соответствующие конструкторы типов параметров, если типы аргументов не являются ожидаемыми.

Очень простой пример этого вызова функции, ожидающей std::stringс const char*аргументом. Компилятор автоматически сгенерирует код для вызова соответствующего std::stringконструктора.

Мне интересно, это так плохо для читабельности, как я думаю, что это?

Вот пример:

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

Это просто нормально? Или это заходит слишком далеко? Если я не должен этого делать, могу ли я заставить Clang или GCC предупредить об этом?

futlib
источник
1
что, если Draw был перегружен строковой версией позже?
фрик с трещоткой
1
согласно ответу @Dave Rager, я не думаю, что это скомпилирует все компиляторы. Смотрите мой комментарий на его ответ. По-видимому, согласно стандарту c ++, вы не можете связывать неявные преобразования, подобные этим. Вы можете сделать только одно преобразование и не более.
Джонатан Хенсон
ОК, извините, на самом деле не скомпилировал это. Обновил пример и все равно ужасно, ИМО.
futlib

Ответы:

24

Это называется конвертирующим конструктором (или иногда неявным конструктором или неявным преобразованием).

Я не знаю переключателя времени компиляции, чтобы предупредить, когда это происходит, но это очень легко предотвратить; просто используйте explicitключевое слово.

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

Относительно того, является ли преобразование конструкторов хорошей идеей: это зависит.

Обстоятельства, при которых неявное преобразование имеет смысл:

  • Класс достаточно дешев, чтобы создать его, и вам все равно, будет ли он создан неявно.
  • Некоторые классы концептуально похожи на свои аргументы (например, std::stringотражают ту же концепцию, из которой const char *он может неявно конвертировать), поэтому неявное преобразование имеет смысл.
  • Некоторые классы становятся намного более неприятными для использования, если неявное преобразование отключено. (Подумайте о необходимости явного вызова std :: string каждый раз, когда вы хотите передать строковый литерал. Части Boost похожи.)

Обстоятельства, при которых неявное преобразование имеет меньше смысла:

  • Строительство стоит дорого (например, ваш пример текстуры, который требует загрузки и анализа графического файла).
  • Классы концептуально очень отличаются от своих аргументов. Рассмотрим, например, массив в виде массива, который принимает в качестве аргумента свой размер:
    Класс FlagList
    {
        FlagList (int initial_size); 
    };

    void SetFlags (const FlagList & flag_list);

    int main () {
        // Теперь это компилируется, хотя это совсем не очевидно
        // что он делает
        SetFlags (42);
    }
  • Строительство может иметь нежелательные побочные эффекты. Например, AnsiStringкласс не должен неявно конструироваться из a UnicodeString, поскольку преобразование Unicode-в-ANSI может потерять информацию.

Дальнейшее чтение:

Джош Келли
источник
3

Это скорее комментарий, чем ответ, но слишком большой, чтобы его можно было добавить в комментарий.

Интересно, g++что не позволяет мне это сделать

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

Производит следующее:

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

Однако, если я изменю строку на:

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

Он выполнит это преобразование.

Дейв Рейгер
источник
Это действительно интересная «особенность» в g ++. Я предполагаю, что это либо ошибка, которая проверяет только один тип на глубину, а не идет рекурсивно вниз, насколько это возможно во время компиляции, чтобы сгенерировать правильный код, или есть флаг, который необходимо установить в вашей команде g ++.
Джонатан Хенсон
1
ru.cppreference.com/w/cpp/language/implicit_cast похоже, что g ++ довольно строго следует стандарту. Компилятор Microsoft или Mac слишком щедр в коде OP. Особо следует сказать следующее утверждение: «При рассмотрении аргумента для конструктора или пользовательской функции преобразования допускается только одна стандартная последовательность преобразования (в противном случае пользовательские преобразования могут быть эффективно связаны цепью)».
Джонатан Хенсон,
Да, я просто собрал код, чтобы протестировать некоторые параметры gccкомпилятора (которые, похоже, не существуют для решения этого конкретного случая). Я не стал вдаваться в подробности (я должен работать :-), но с учетом gccсоблюдения стандарта и использования explicitключевого слова опция компилятора, вероятно, была сочтена ненужной.
Дэйв Рейджер
Неявные преобразования не связаны друг Textureс другом , и, вероятно, не следует создавать их неявно (в соответствии с рекомендациями в других ответах), так что лучше будет сайт вызова renderer.Draw(Texture("foo.png"));(при условии, что он работает так, как я ожидал).
Blaisorblade
3

Это называется неявным преобразованием типов. В целом это хорошо, так как препятствует ненужному повторению. Например, вы автоматически получаете std::stringверсию Drawбез необходимости писать для нее дополнительный код. Это также может помочь в следовании принципу открытого-закрытого, поскольку позволяет расширять Rendererвозможности без изменения Rendererсамого себя.

С другой стороны, это не лишено недостатков. С одной стороны, это может затруднить выяснение источника аргументов. Иногда это может привести к неожиданным результатам в других случаях. Вот для чего explicitключевое слово. Если вы поместите его в Textureконструктор, он отключит использование этого конструктора для неявного преобразования типов. Мне не известен метод глобального предупреждения о неявном преобразовании типов, но это не значит, что метод не существует, только то, что у gcc есть непостижимо большое количество опций.

Карл Билефельдт
источник