Какой конструктор когда вызывается в С++

С++ имеет весьма разнообразный синтаксис для конструирования объектов. Надо признать, что порой этот синтаксис весьма неочевиден, и многие вещи надо просто знать, нежели догадаться, как они работают. Например:

class T {...};
...
T t = T(1);

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

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

Именно для таких случаев я обычно даю следующий пример, который покрывает часто используемые варианты создания объектов. Разобрав его один раз целиком, можно использовать его как подсказку в будущем, когда опять возникает вопрос “а что ж здесь будет вызвано: конструктор или оператор копирования?…“.

Итак, файл ctor.cpp:

#include <iostream>

class T {
public:
  T() { std::cout << "T()" << std::endl; }
  T(int) { std::cout << "T(int)" << std::endl; }
  T(int, int) { std::cout << "T(int, int)" << std::endl; }
  T(const T&) { std::cout << "T(const T&)" << std::endl; }
  void operator=(const T&) 
    { std::cout << "operator=(const T&)" << std::endl; }
};

int main() {
  std::cout << "T t1           : "; T t1;
  std::cout << "T t2(1)        : "; T t2(1);
  std::cout << "T t3 = 1       : "; T t3 = 1;
  std::cout << "T t4 = T(1)    : "; T t4 = T(1);
  std::cout << "T t5(1, 2)     : "; T t5(1, 2);
  std::cout << "T t6 = T(1, 2) : "; T t6 = T(1, 2);
  std::cout << "T t7; t7 = 1   : "; T t7; t7 = 1;
  std::cout << "T t8; t8 = T(1): "; T t8; t8 = T(1);
  std::cout << "T t9(t8)       : "; T t9(t8);
  std::cout << "T t10 = 'a'    : "; T t10 = 'a';
  return 0;
}

Компилируем, например в Visual Studio:

cl /EHsc ctor.cpp

и запускаем:

T t1           : T()
T t2(1)        : T(int)
T t3 = 1       : T(int)
T t4 = T(1)    : T(int)
T t5(1, 2)     : T(int, int)
T t6 = T(1, 2) : T(int, int)
T t7; t7 = 1   : T()
T(int)
operator=(const T&)
T t8; t8 = T(1): T()
T(int)
operator=(const T&)
T t9(t8)       : T(const T&)
T t10 = 'a'    : T(int)

Видно, что во всех этих “разнообразных” способах создания объекта всегда вызывался непосредственно конструктор, а не оператор копирования. Оператор же копирования был вызван только когда знак присваивания использовался явно в отдельном от вызова конструктора операторе. То есть знак “=”, используемый в операторе конструирования объекта так или иначе приводит к вызову конструкторов, а не оператора копирования. И это происходит вне зависимости от какой-либо оптимизации, проводимой компилятором.

Также интересно, как был создана переменная t10. Видно, что для символьной константы компилятор “подобрал” наиболее подходящий конструктор. Неявным образом был вызвал конструктор от int. Если подобное поведение не входит в ваши планы, и вам совсем не нужно, чтобы конструктор от int вызывался, когда идет попытка создать объект от типа, который может быть неявно преобразован в int, например char, то можно воспользоваться ключевым словом explicit:

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

Это запретит какое-либо неявное преобразования для аргумента этого конструктора.

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

Я обычно по умолчанию пишу explicit для конструкторов с одним параметром, и очень редко приходится потом убирать этого слово. Тут как со словом const — сначала можно написать, а потом уже думать нужно ли тут его убрать или нет.


Оригинальный пост | Disclaimer

Комментарии