почему неявное преобразование вредно в C ++ - PullRequest
23 голосов
/ 27 февраля 2010

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

Например

Foo {

 public:
 explicit Foo(int i) {}
}

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

Ответы [ 8 ]

11 голосов
/ 27 февраля 2010

Используйте 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;
};

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

8 голосов
/ 27 февраля 2010

Вводит неожиданные временные значения :

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 ));
5 голосов
/ 27 февраля 2010

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

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.

3 голосов
/ 27 февраля 2010

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

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

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

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

2 голосов
/ 27 февраля 2010

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

1 голос
/ 27 февраля 2010

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

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

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

0 голосов
/ 27 февраля 2010

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

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

0 голосов
/ 27 февраля 2010

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

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) создает пустую строку. Если вы пометите конструктор как явный, этот тип преобразования отключен. Так что будьте осторожны с неявными преобразованиями - знайте все типы утверждений, которые это разрешит.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...