Какова причина того, что копирование и прямая инициализация ведут себя по-разному?
Несколько связано с тем, почему конструктор копирования вызывается вместо конструктора преобразования?
Существует два синтаксиса для инициализации, прямой инициализации и инициализации копирования:
A a(b);
A a = b;
Я хочу знать мотивацию для них различного определенного поведения. Для инициализации копии требуется дополнительная копия, и я не могу думать о какой-либо цели для этой копии. Так как это копия из временного хранилища, она может и, вероятно, будет оптимизирована, поэтому пользователь не может рассчитывать на то, что это произойдет - поэтому сама дополнительная копия не является достаточной причиной для такого поведения. Итак... почему?
4 ответа
Так как это копия из временного каталога, она может и, вероятно, будет оптимизирована
Ключевое слово здесь, вероятно. Стандарт позволяет, но не требует, компилятор оптимизировать копию. Если бы некоторые компиляторы разрешали этот код (оптимизированный), а другие отклоняли его (не оптимизированный), это было бы очень противоречивым.
Таким образом, стандарт предписывает последовательный способ обработки этого - каждый должен проверить, доступен ли конструктор копирования, независимо от того, используют ли они его тогда или нет.
Идея состоит в том, что все компиляторы должны либо принять код, либо отклонить его. В противном случае это будет непереносимо.
Еще один пример, рассмотрим
A a;
B b;
A a1 = a;
A a2 = b;
Было бы одинаково непоследовательным a2
но запретить a1
когда A
Конструктор копирования является закрытым.
Из стандартного текста также видно, что два метода инициализации объекта класса должны были отличаться (8.5/16):
Если инициализация - это прямая инициализация, или если это инициализация копирования, где не квалифицированная версия cv исходного типа является тем же классом или производным классом класса назначения, рассматриваются конструкторы. Применимые конструкторы перечислены (13.3.1.3), и лучший выбирается через разрешение перегрузки (13.3). Выбранный таким образом конструктор вызывается для инициализации объекта с выражением инициализатора или списком выражений в качестве аргументов. Если конструктор не применяется, или разрешение перегрузки неоднозначно, инициализация некорректна.
В противном случае (т. Е. Для остальных случаев инициализации копирования) определяемые пользователем последовательности преобразования, которые могут преобразовывать из типа источника в тип назначения или (когда используется функция преобразования) в его производный класс, перечисляются, как описано в 13.3.1.4, а лучший выбирается с помощью разрешения перегрузки (13.3). Если преобразование не может быть выполнено или является неоднозначным, инициализация неверна. Выбранная функция вызывается с выражением инициализатора в качестве аргумента; если функция является конструктором, вызов инициализирует временную версию cv-unqualified версии назначения. Временное является prvalue. Результат вызова (который является временным для случая конструктора) затем используется для прямой инициализации, в соответствии с приведенными выше правилами, объекта, который является местом назначения инициализации копирования. В некоторых случаях реализация позволяет исключить копирование, присущее этой прямой инициализации, путем создания промежуточного результата непосредственно в инициализируемом объекте; см. 12.2, 12.8.
Разница заключается в том, что при прямой инициализации непосредственно используются конструкторы построенного класса. При инициализации копирования рассматриваются другие функции преобразования, и они могут создавать временное копирование.
Только предположение, но я боюсь, что будет трудно быть более уверенным без Бьярне Страуструпа, подтверждающего, как это было на самом деле:
Он был спроектирован таким образом, потому что предполагалось, что такое поведение будет ожидать программист, что он будет ожидать, что копирование будет выполнено при использовании знака =, а не с использованием синтаксиса прямого инициализатора.
Я думаю, что возможное удаление копии было добавлено только в более поздних версиях стандарта, но я не уверен - это то, что кто-то может сказать наверняка, проверив стандартную историю.
Возьмите следующий пример:
struct X
{
X(int);
X(const X&);
};
int foo(X x){/*Do stuff*/ return 1; }
X x(1);
foo(x);
В протестированных мной компиляторах аргумент foo
всегда копировался даже при включенной полной оптимизации. Исходя из этого, мы можем заключить, что копии не будут / не должны удаляться во всех ситуациях.
Теперь давайте подумаем с точки зрения языкового дизайна, представьте все сценарии, о которых вам нужно подумать, если вы хотите создать правила для случаев, когда копия нужна, а когда - нет. Это было бы очень сложно. Кроме того, даже если бы вы смогли придумать правила, они были бы очень сложными и почти невозможными для понимания людьми. Однако, в то же время, если бы вы заставляли копировать везде, это было бы очень неэффективно. Вот почему правила такие, какие есть, вы делаете правила понятными для людей, но при этом не заставляете их делать копии, если их можно избежать.
Я должен признать, что этот ответ очень похож на ответ Сума. Идея состоит в том, что вы можете ожидать поведения с текущими правилами, и людям будет трудно следовать чему-либо еще.
Инициализация встроенных типов, таких как:
int i = 2;
Это очень естественный синтаксис, отчасти по историческим причинам (помните математику в старшей школе). Это более естественно, чем:
int i(2);
даже если некоторые математики могут спорить об этом. В конце концов, нет ничего противоестественного в вызове функции (в данном случае конструктора) и передаче ей аргумента.
Для встроенных типов эти два типа инициализации идентичны. В первом случае нет дополнительной копии. По этой причине инициализируются оба типа, и изначально не было особого намерения заставить их вести себя по-разному.
Однако существуют пользовательские типы, и одна из заявленных целей языка - позволить им вести себя как встроенные типы настолько близко, насколько это возможно.
Таким образом, конструкция копирования (например, принимая входные данные от некоторой функции преобразования) является естественной реализацией первого синтаксиса.
Тот факт, что у вас могут быть дополнительные копии и что они могут быть исключены, является оптимизацией для пользовательских типов. И копирование elision, и явные конструкторы появились намного позже в языке. Неудивительно, что стандарт допускает оптимизацию после определенного периода использования. Кроме того, теперь вы можете исключить явные конструкторы из кандидатов на разрешение перегрузки.