Перефразируя наш вопрос:
почему мы должны писать Date(const Date &)
вместо Date(Date)
?
Я собираюсь разделить это на две части: первая отвечает, почему конструктор копирования должен принимать свой аргумент для каждой ссылки, вторая - почему это должна быть константная ссылка.
Причина, по которой конструктор копирования должен принимать свой аргумент для каждой ссылки, состоит в том, что для функции, которая принимает аргумент для каждой копии void f(T arg)
, когда вы вызываете ее f(obj)
, obj
- это скопировано в arg
с использованием конструктора копирования T
. Поэтому, если вы хотите реализовать конструктор копирования, вам лучше не принимать аргумент за копированием, потому что это вызовет конструктор копирования при его вызове, что приведет к бесконечной рекурсии. Вы можете легко попробовать это сами:
struct tester {
tester(tester) {std::cout << "inside of erroneous copy ctor\n";}
};
int main()
{
tester t1;
std::cout << "about to call erroneous copy ctor\n";
tester t2(t1);
std::cout << "done with call erroneous copy ctor\n";
return 0;
}
Эта программа должна когда-либо писать только одну строку, а затем уничтожать стек.
Примечание: Как отмечает Деннис в своем комментарии, на самом деле эта программа не гарантируется для компиляции, поэтому, в зависимости от вашего компилятора, вы не сможете ее попробовать.
Итог: Конструктор копирования должен принимать свой аргумент по ссылке , потому что для получения его за копию потребуется конструктор копирования.
Это оставляет вопрос почему это const T&
, а не просто T&
? На самом деле, для этого есть две причины.
Логическая причина заключается в том, что при вызове конструктора копирования вы не ожидаете, что скопированный объект изменится. В C ++, если вы хотите выразить, что что-то является неизменным, вы используете const
. Это говорит пользователям, что они могут безопасно передавать свои драгоценные объекты вашему конструктору копирования, потому что он не будет ничего с ним делать, кроме чтения из него. В качестве приятного побочного эффекта, если вы реализуете конструктор копирования и случайно попытаетесь записать объект, компилятор выдаст вам сообщение об ошибке, напоминающее вам об обещании, сделанном для вызывающей стороны.
Другая причина в том, что вы не можете привязать временные объекты к ссылкам не const
, вы можете привязать их только к const
ссылкам. Например, временный объект - это то, что может вернуть функция:
struct tester {
tester(tester& rhs) {std::cout << "inside of erroneous copy ctor\n";}
};
tester void f()
{
tester t;
return t;
}
Когда вызывается f()
, внутри него создается объект tester
, и его копия затем возвращается вызывающей стороне, которая затем может поместить ее в другую копию:
tester my_t = f(); // won't compile
Проблема в том, что f()
возвращает временный объект, и чтобы вызвать конструктор копирования, этот временный объект должен будет привязаться к аргументу rhs
конструктора копирования tester
, который не является const
ссылка. Но вы не можете связать временный объект со ссылкой не const
, поэтому код не будет компилироваться.
Хотя вы можете обойти это, если хотите (просто не копируйте временный объект, а вместо этого привязайте его к ссылке const
, которая продлевает время существования временного элемента до конца времени существования ссылки: const tester& my_t = f()
), люди ожидают уметь копировать временные файлы вашего типа.
Итог: Конструктор копирования должен принимать свой аргумент по константной ссылке , потому что в противном случае пользователи могут не захотеть или не смогут его использовать.
Вот еще один факт: в следующем стандарте C ++ вы можете перегрузить функции для временных объектов, так называемые rvalues
. Таким образом, вы можете иметь специальный конструктор копирования, который принимает временные объекты, перегружающие «нормальный» конструктор копирования. Если у вас есть компилятор, который уже поддерживает эту новую функцию, вы можете попробовать его:
struct tester {
tester(const tester& rhs) { std::cout << "common copy ctor\n"; }
tester( tester&& rhs) { std::cout << "copy ctor for rvalues\n"; }
};
Когда вы используете приведенный выше код для вызова нашего f()
tester my_t = f();
новый конструктор копирования для rvalues должен вызываться, когда временный объект, возвращаемый вызовом f()
, копируется в my_t
, и обычный конструктор копирования может быть вызван для копирования t
объект изнутри f()
в возвращенный временный объект. (Примечание: вам может потребоваться отключить оптимизацию вашего компилятора, чтобы увидеть это, поскольку компилятору разрешено оптимизировать все операции копирования.)
Так что вы можете с этим? Что ж, когда вы копируете значение r, вы знаете, что скопированный объект будет уничтожен после вызова конструктора копирования, поэтому конструктор копирования, принимающий значение r (T&&
), может просто украсть значения из аргумента вместо их копирования. Поскольку объект все равно будет уничтожен, никто не заметит.
Для некоторых классов (например, для строковых классов) перемещение значения от одного объекта к другому может быть намного дешевле, чем копирование их.