суббота, 1 января 2011 г.

explicit constructor

Давайте рассмотрим как отличаются явные и неявные конструкторы. Многие посмотрев на код ниже подумают, что различий между инициализацией a и b не очень много:
class SomeClass {
public:
  SomeClass( int );
};

int main() {
  SomeClass a( 5 );
  SomeClass b = 5; // если конструктор помечен explicit, то тут будет ошибка

  return 0;
}
Я имею ввиду, что в случае инициализации b не будет вызываться оператор копирования, как думают многие начинающие разработчики. Обе этих строчки вызывают конструктор с той лишь разницей, что в первом случае — это явный(explicit) вызов, в во втором — неявный(implicit).

Cледующий пример поможет продемонстрировать как эта разница может проявиться в виде ошибки на практике:
#include <iostream>

class sss
{
public:
  explicit sss( int ) {
    std::cout << "int" << std::endl;
  }

  sss( double ) {
    std::cout << "double" << std::endl;
  }
};

int main()
{
  sss ddd( 7 );
  sss xxx = 7;

  return 0;
}
Известно, что все целые числа по стандарту автоматически имеют тип int, если иного явно не указано. Т.е. число 7 в примере выше имеет тип int. Поэтому, кажется на первый взгляд, что в обоих случаях будет вызван конструктор принимающий int. Однако, это не так.

Запись вида «sss ddd( 7 );» является явным вызовом конструктора, а «sss xxx = 7;» — неявным. Если бы конструктор с int был в private секции, то была бы выдана ошибка. Но ключевое слово explicit скывает конструктор так незаметно, что даже предупреждения компилятор не выдаст. Если скомпилировать и запустить пример, то окажется, что во втором случает будет вызван конструктор, принимающий double. Это может оказаться довольно неожиданным поворотом событий, если не разбираться в формах вызовов конструкторов. Часто разработчики не придают особого значения как они конструируют объект.

Если написать explicit для обоих конструкторов, то ошибки можно будет избежать — компилятор подскажет где ошибка. Поэтому explicit следует писать для всех конструкторов с одним параметром, если специально не предполагается другое поведение. Мне кажется, что это стоило делать по умолчанию в стандарте, а для других случаев ввести что-то вроде nonexplicit.

7 комментариев:

  1. explicit и было введено, если мне неизменяет память для того чтобы запретить подобное поведение, если оно нежелательно.

    ОтветитьУдалить
  2. explicit указывает на требования явной формы конструктора копий.

    Я пишу о том, что оно ещё и скрывает конструктор. Т.е. я бы хотел, чтобы компилятор выдавал ошибку о том, что требуется явный вызов, а не вызывал по тихому конструктор копий для double...

    ОтветитьУдалить
  3. ну ошибкой считалось бы наверняка если бы отсутствовал второй конструктор. А в приведенном примере даже ворнинга о преобразовании int в double выдаваться не будет, да и не должно. Если бы double->int то некоторые компиляторы выдают предупреждения из серии "loss of data".

    ОтветитьУдалить
  4. Yuriy Volkov! Попробуйте еще раз вникнуть в суть статьи. Существует два конструктора. В обоих случаях автор кода хочет вызывать тот, что принимает на вход int. Это следует из того, что в качестве аргумента выступает число 7 (а не 7.0, например). Ожидаемое поведение компилятора - это ошика, так как конструктор для int объявлен как explicit, потому не может быть скомпилирован вариант "sss xxx = 7;". Однако, компилиятор делает нехорошую вещь: он выбрасывает из перегрузки конструктор для int (а по какому праву?) и рассматривает только конструктор с double. Компилятор должен видеть, что конснтруктор для int существует, что автор кода вызывает именно его и ругаться, что такой вызов незаконный, так как он объявлен explicit. Этого ожидает разработчик, вель ровно так и происходит, если вместо explicit вы объявите конструктор закрытым: компилятор не закроет глаза на то, что конструктор теперь private, вызвав public-вариант с double-аргументом, он выругается и поделом! Хотелось бы аналогичного поведения и для explicit, но увы, С++ ведет себя не так.

    ОтветитьУдалить
  5. ANSI ISO/IEC 14882:2003(E)
    12.3.1 Conversion by constructor
    A constructor declared without the function-specifier explicit that can be called with a single parameter specifies a conversion from the type of its first parameter to the type of its class. Such a constructor is called a converting constructor.

    class X {
    // ...
    public:
    X(int);
    X(const char*, int =0);
    };

    An explicit constructor constructs objects just like non-explicit constructors, but does so only where the direct-initialization syntax (8.5) or where casts (5.2.9, 5.4) are explicitly used. A default constructor may be an explicit constructor; such a constructor will be used to perform default-initialization or value initialization
    (8.5).

    и далее по тексту. Это ответ на вопрос "А по какому праву?". По тому какой синтаксис был использован в выражении (sss xxx = 7;) компилятор определяет какой конструктор должен быть вызван. Про неявное преобразование типов рассказывать думаю будет лишним.

    ОтветитьУдалить
  6. Все верно, но суть была в том, что легко ошибится и получить неожиданный результат.

    ОтветитьУдалить
  7. да в С++ вообще много где можно ошибиться ;-)

    ОтветитьУдалить