Подтвердить что ты не робот

Почему неявное преобразование вредно в С++

Я понимаю, что ключевое слово явное может использоваться для предотвращения неявного преобразования.

Например

Foo {

 public:
 explicit Foo(int i) {}
}

Мой вопрос заключается в том, при каких условиях неявное преобразование должно быть запрещено? Почему неявное преобразование вредно?

4b9b3361

Ответ 1

Используйте explicit, если вы предпочитаете компиляцию ошибки.

explicit применим только в том случае, если в вашем конструкторе есть один параметр (или многие, где первый является единственным без значения по умолчанию).

Вы хотели бы использовать ключевое слово explicit в любое время, когда программист может по ошибке создать объект, думая, что он может делать то, чего он на самом деле не делает.

Вот пример:

class MyString
{
public:
    MyString(int size)
        : size(size)
    {
    }

     //... other stuff

    int size;
};

С помощью следующего кода вы можете сделать это:

int age = 29;
//...
//Lots of code
//...
//Pretend at this point the programmer forgot the type of x and thought string
str s = x;

Но вызывающий объект, вероятно, должен был хранить "3" внутри переменной MyString, а не 3. Лучше получить компиляционную ошибку, чтобы пользователь мог сначала вызвать itoa или какую-либо другую функцию преобразования для переменной x.

Новый код, который приведет к компиляционной ошибке для вышеуказанного кода:

class MyString
{
public:
    explicit MyString(int size)
        : size(size)
    {
    }

     //... other stuff

    int size;
};

Ошибки компиляции всегда лучше ошибок, потому что они сразу видны для вас.

Ответ 2

В нем представлены неожиданные временные рамки:

struct Bar
{
    Bar();      // default constructor
    Bar( int ); // value constructor with implicit conversion
};

void func( const Bar& );

Bar b;
b = 1; // expands to b.operator=( Bar( 1 ));
func( 10 ); // expands to func( Bar( 10 ));

Ответ 3

Пример реального мира:

class VersionNumber
{
public:
    VersionNumber(int major, int minor, int patch = 0, char letter = '\0') : mMajor(major), mMinor(minor), mPatch(patch), mLetter(letter) {}
    explicit VersionNumber(uint32 encoded_version) { memcpy(&mLetter, &encoded_version, 4); }
    uint32 Encode() const { int ret; memcpy(&ret, &mLetter, 4); return ret; }

protected:
    char mLetter;
    uint8 mPatch;
    uint8 mMinor;
    uint8 mMajor;
};

VersionNumber v = 10; почти наверняка будет ошибкой, поэтому ключевое слово explicit требует, чтобы программист набрал VersionNumber v(10); и - если он или она использует достойную среду IDE - они будут видеть через всплывающее окно IntelliSense, что он хочет encoded_version.

Ответ 4

В основном неявное преобразование является проблемой, когда он позволяет компилировать код (и, вероятно, делать что-то странное) в ситуации, когда вы делали что-то, чего не намеревались, и предпочли бы, чтобы код не компилировался, но вместо этого некоторое преобразование позволяет код для компиляции и сделать что-то странное.

Например, iostreams имеют преобразование в void *. Если вы немного устали и введите что-то вроде: std::cout << std::cout;, он действительно скомпилирует - и произведет какой-то бесполезный результат - обычно что-то вроде шестнадцатеричного числа с 8 или 16 цифрами (8 цифр в 32-битной системе, 16 цифр на 64-битной системе).

В то же время я чувствую себя обязанным отметить, что многие люди, похоже, почти рефлексивно отвращаются к неявным преобразованиям любого рода. Существуют классы, для которых неявные преобразования имеют смысл. Прокси-класс, например, позволяет преобразовать другой тип. Преобразование в этот тип никогда не бывает неожиданным для прокси-сервера, потому что это просто прокси-сервер, т.е. Это то, что вы можете (и должны) считать полностью эквивалентным типу, для которого он является прокси-сервером, за исключением, конечно, он должен реализовать какое-то особое поведение для какой-то конкретной ситуации.

Например, несколько лет назад я написал класс bounded<T>, который представляет (целочисленный) тип, который всегда остается в пределах заданного диапазона. Другое, которое отказывается присвоить значение за пределами указанного диапазона, оно действует точно так же, как и основной тип intger. Он делает это (в основном), предоставляя неявное преобразование в int. Почти все, что вы с ним сделаете, будет действовать как int. По сути, единственное исключение - это когда вы назначаете ему значение - тогда он выдает исключение, если значение вне диапазона.

Ответ 5

Это не вредно для опытных. Может быть вредным для начинающего или более свежей отладки другого кода.

Ответ 6

"Вредный" - сильное утверждение. "Не то, что можно использовать без мысли" - это хорошо. Большая часть С++ такова (хотя некоторые могут утверждать, что некоторые части С++ вредны...)

В любом случае, худшая часть неявного преобразования заключается в том, что это может произойти не только тогда, когда вы этого не ожидаете, но, если я не ошибаюсь, он может цепочки... пока существует неявный путь перехода между типом Foo и тип Bar, компилятор найдет его и преобразует по этому пути, что может иметь много побочных эффектов, которых вы не ожидали.

Если единственное, что у вас получается, - это не нужно вводить несколько символов, это просто не стоит. Быть явным означает, что вы знаете, что на самом деле происходит, и не получите бит.

Ответ 7

Чтобы развернуть ответ Брайана, подумайте, что у вас есть это:

class MyString
{
public:
    MyString(int size)
        : size(size)
    {
    }

    // ...
};

Фактически это позволяет компилировать этот код:

MyString mystr;
// ...
if (mystr == 5)
    // ... do something

У компилятора нет оператора == для сравнения MyString с int, но он знает, как сделать MyString из int, поэтому он смотрит на оператор if следующим образом:

if (mystr == MyString(5))

Это очень вводит в заблуждение, поскольку похоже, что он сравнивает строку с числом. На самом деле этот тип сравнения, вероятно, никогда не бывает полезен, предполагая, что конструктор MyString (int) создает пустую строку. Если вы помечаете конструктор как явный, этот тип преобразования отключен. Поэтому будьте осторожны с неявными преобразованиями - будьте в курсе всех типов утверждений, которые он позволит.

Ответ 8

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

Вредный - немного сильное слово для неявных преобразований. Это вредно не столько для первоначальной реализации, сколько для обслуживания приложений. Неявные преобразования позволяют компилятору молча менять типы, особенно в параметрах, на еще один вызов функции - например, автоматически преобразуя int в некоторый другой тип объекта. Если вы случайно передадите int в этот параметр, компилятор "поможет" тихо создать временное для вас, оставив вас в недоумении, когда все будет работать неправильно. Конечно, мы все можем сказать "о, я никогда не сделаю эту ошибку", но нужно лишь один раз отлаживать часами, прежде чем начинать думать, возможно, что компилятор говорит вам об этих преобразованиях, это хорошая идея.