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 — сначала можно написать, а потом уже думать нужно ли тут его убрать или нет.
По очевидной логике вещей данный код должен при создании экземпляра класса T вызвать конструктор по умолчанию (без аргументов), затем создать временный объект с помощью конструктора с одним аргументом и скопировать его в исходный объект перегруженным оператором копирования (или может конструктором копирования? ведь слева и справа объекты явно типа T...).
ОтветитьУдалитьпоясните откуда такая логика вообще возникла ? зачем так сложно ?
Как мне кажется, человеку свойственно читать как написано, и если он не знает заранее, как там что-то должно истолковываться, то делается попытка истолковать просто исходя из семантики прочитанного. Представьте, что человек не знает, как действительно работает в С++ выражение "T t = T(1);". Согласитесь, что он скорее всего пойдет его как я описал, нежели "данное выражение просто создает экземпляр класса T и вызывает конструктор от int". Тогда для чего тут знак присваивание? Двойное употребление слова "T"? Что-то запутать понимание?
ОтветитьУдалитьСогласен, это просто вопрос знания правил, но лучше когда правила логичны и следуют за синтаксисом, а не требуют дополнительного описания "как это все на самом деле работает". Разве нет?
скорее не соглашусь с вами:
ОтветитьУдалитьT t <- декларация переменной типа t
скорее для человека не знакомого с C++ логика будет такая, что
T t; он воспримет просто как декларацию переменной без инициализации, а
T t = T(1) именно как инициализацию с вызовом конструктора от int
на мой взгляд, про операторы присвоения, копирования и т.п вспомнил бы значительно после.
ps. попросил своих знакомых войти в образ начинающего программиста по Станиславскому и спросил, что они думают о T t = T(1) - говорят, что предложенная вами очевидная логика не мегаочевидна :)
в целом статья познавательная - ибо сам, когда пришёл в мир C++ разбирался с этим
ОтветитьУдалить