*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

воскресенье, 15 марта 2009 г.

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

С++ имеет весьма разнообразный синтаксис для конструирования объектов. Надо признать, что порой этот синтаксис весьма неочевиден, и многие вещи надо просто знать, нежели догадаться, как они работают. Например:
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 — сначала можно написать, а потом уже думать нужно ли тут его убрать или нет.

4 комментария:

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

    поясните откуда такая логика вообще возникла ? зачем так сложно ?

    ОтветитьУдалить
  2. Как мне кажется, человеку свойственно читать как написано, и если он не знает заранее, как там что-то должно истолковываться, то делается попытка истолковать просто исходя из семантики прочитанного. Представьте, что человек не знает, как действительно работает в С++ выражение "T t = T(1);". Согласитесь, что он скорее всего пойдет его как я описал, нежели "данное выражение просто создает экземпляр класса T и вызывает конструктор от int". Тогда для чего тут знак присваивание? Двойное употребление слова "T"? Что-то запутать понимание?

    Согласен, это просто вопрос знания правил, но лучше когда правила логичны и следуют за синтаксисом, а не требуют дополнительного описания "как это все на самом деле работает". Разве нет?

    ОтветитьУдалить
  3. скорее не соглашусь с вами:
    T t <- декларация переменной типа t
    скорее для человека не знакомого с C++ логика будет такая, что
    T t; он воспримет просто как декларацию переменной без инициализации, а
    T t = T(1) именно как инициализацию с вызовом конструктора от int

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

    ps. попросил своих знакомых войти в образ начинающего программиста по Станиславскому и спросил, что они думают о T t = T(1) - говорят, что предложенная вами очевидная логика не мегаочевидна :)

    ОтветитьУдалить
  4. в целом статья познавательная - ибо сам, когда пришёл в мир C++ разбирался с этим

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