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++ разбирался с этим
ОтветитьУдалить