- Какая разница между
пуганой вороной и письменным столом?
"Вот это совсем
другой разговор! - подумала Алиса.- Загадки-то я люблю! Поиграем!"
- Кажется, сейчас
отгадаю,- прибавила она вслух.
- Ты думаешь, что могла бы отыскать
отгадку? - удивленно спросил Заяц...
Льюис Керролл "Алиса в стране
чудес"
Чем отличается
выражение
1) Entity cowedCrow( 1);
от выражения
2) Entity desk = 1;
кроме названия
объектов, конечно же?
На
удивление многие программисты дают неверный ответ. Менее опытные ошибочно
полагают, что во втором случае сначала вызывается конструктор без аргументов, а
затем оператор присваивания. Более опытные наивно полагают, что данные
выражения эквивалентны (по сути, не отличаются). И, наконец, опытные убеждены,
что знают правильный ответ, и готовы прекратить чтение статьи.
Что
же, попробуем доставить немного удовольствия опытным программистам, а заодно и развеять
заблуждения менее опытных.
Рассмотрим
возможное определение класса Entity:
using namespace std; //
Стыдимся, но для примера сойдет
class Entity {
public:
Entity(
int number) {
cout
<< "ctor: " << number
<< endl;
}
Entity(
const Entity &) {
cout
<< "copy ctor" <<
endl;
}
private:
Entity
& operator= ( const Entity &);
};
Отметим,
что при такой реализации выражения 1) и 2) успешно компилируются, несмотря на
то, что Entity
не
имеет конструктора по умолчанию и его оператор присваивания недоступен. Это
происходит потому, что оба выражения, согласно стандарту С++, описывают инициализацию
(создание) объекта. Таким образом, в выражении 2) оператор присваивания не
вызывается, поскольку объект desk
еще
не создан.
Выполнение
выражений 1) и 2) приводит к выводу на консоль:
ctor: 1
ctor: 1
Это
именно тот результат, который ожидали получить более опытные программисты. Но
значит ли это, что выражения 1) и 2) эквивалентны? Изменим интерфейс класса Entity следующим
образом:
class Entity {
public:
// Как и раньше
private:
Entity( const Entity &) { // Теперь недоступен вне класса
// Как и
раньше
}
// Как и раньше
};
Мы
закрыли доступ к конструктору копирования, что привило к тому, что компилятор gcc 4 выдал ошибку в выражении 2), ссылаясь
на недоступный конструктор копирования. Компилятор VC10 при этом откомпилировал этот же код
без ошибок. Для того, чтобы понять, какой компилятор прав, вновь обратимся к
стандарту С++.
Выражение
1) Entity cowedCrow( 1); описывает
вид инициализации объекта, называемый прямой инициализацией (direct initialization) и подразумевает явный
вызов конструктора.
Выражение
2) Entity desk = 1; описывает вид
инициализации объекта, называемый инициализацией копированием (copy initialization) и подразумевает неявный
вызов конструктора копирования.
Таким
образом, компилятор gcc 4 поступает
в соответствии со стандартом. Поведение VC10 объясняется тем, что в Visual Studio 2010 по умолчанию включены расширения
языка (Project->Properties->C/C++->Language->Disable Language Extensions = No). Если эти расширения отключить, то
результат будет аналогичен gcc 4.
Итак,
мы выяснили, что выражение 2) подразумевает вызов конструктора копирования, так
почему же результат выполнения выражений 1) и 2) в примере выше одинаковый
(вызова конструктора копирования в выражении 2) не было)? Дело в том, что по
стандарту компилятору разрешено производить преобразования с целью исключения
создания временных объектов и обращения к конструктору копирования и
генерировать тот же самый код, что и при прямой инициализации. И хотя
большинство компиляторов выполняют такое преобразование, стандарт этого не
требует. А это значит, что могут найтись компиляторы, которые при
выполнении выражения 2) вызовут конструктор копирования.
Для
того, чтобы окончательно убедить вас в том, что результат выполнения выражения
2) – результат преобразований компилятора, рассмотрим результаты выполнения
следующих выражений (компилятор VC10,
конструктор копирования объявлен в секции public):
3) Entity desk2 = Entity( 2);
4) Entity cowedCrow2( Entity( 3));
На
консоль будет выведено (ни в одном случае конструктор копирования не вызывается):
ctor: 2
ctor: 3
Отметим
также и тот факт, что хотя VC10 с
включенными расширениями языка и закрытым конструктором копирования без проблем
компилирует выражение Entity desk = 1, он
не может сделать того же для выражения Entity desk2 = Entity( 2).
Возвращаясь
к рассматриваемому преобразованию, заметим, что компилятор применяет его после
того, как проверит семантику программы. Вот почему компилятор выдавал ошибку на
выражении Entity desk = 1; при недоступном
конструкторе копирования. Логика рассуждений компилятора в данном случае
примерно такая:
1) создать
из 1 временный объект класса Entity: Entity(1);
2) создать
объект desk,
копированием Entity(1) в
desk;
3) проанализировать
возможность преобразования и выполнить его: исключить создание временного Entity(1) и, таким образом, создать из 1
объект desk
напрямую.
До
шага 3) компилятор не доходит, поскольку не может выполнить шаг 2) при
недоступном конструкторе копирования.
Итак,
одно из отличий выражений 1) и 2) в том, что выражение 2) может не
откомпилироваться в случае, когда конструктор класса Entity недоступен, а при доступном
конструкторе копирования может привести к его вызову. Есть ли какие-нибудь
другие отличия? Как оказывается – да! Обратите внимание на слово «неявный» в
названии статьи. Давайте посмотрим, что оно означает. Изменим интерфейс класса Entity следующим образом:
class Entity {
public:
explicit Entity(
int number) { // Теперь разрешены только явные вызовы
// Как и раньше
}
Entity( const Entity &) {
// Как и раньше
}
private:
// Как и раньше
};
Выражение
Entity desk = 1; больше не
компилируется, поскольку для выполнения шага 1) (см. выше) компилятору
требуется неявно вызвать конструктор Entity( int) для создания временного объекта Entity. При этом проблем с вызовом Entity desk2 = Entity( 2) не
возникает, поскольку на внутренние преобразования компилятора не влияет тот
факт, что конструктор Entity
объявлен
явным.
В
принципе, мы можем пойти еще дальше и объявить конструктор копирования явным:
class Entity {
public:
explicit Entity(
int number) {
// Как и раньше
}
explicit Entity(
const Entity &) {
// Как и раньше
}
private:
// Как и раньше
};
Такая
конструкция обеспечивает полный запрет неявной инициализации (инициализации
копированием): ни Entity desk = 1; ни Entity desk2 = Entity( 2) больше
не компилируются (даже в VC10 с
включенными расширениями языка). Доступной при этом остается только прямая
инициализация. Зачем может потребоваться специально запрещать синтаксис
инициализации копированием – отдельный вопрос, однако суть в том, что раз язык
позволяет, то всегда существует вероятность того, что подобная конструкция
языка будет где-нибудь использована.
Таким
образом, следующее отличие выражений 1) и 2) в том, что синтаксис выражения 2)
можно запретить (сделав его ошибочным).
Анализируя
отличия прямой инициализации и инициализации копированием можно сделать
следующий вывод: за исключением тривиальных случаев (например, встроенные типы
языка) следует отдавать предпочтение синтаксису прямой инициализации. Это
правило особенно важно в шаблонных выражениях. Однако и при инициализации
конкретных классов оно не утрачивает актуальности: часто существует вероятность
того, что в процессе разработки/сопровождения программы интерфейс класса
изменится (например, конструктор класса станет явным или конструктор
копирования станет недоступным) и инициализация копированием перестанет
работать.
"Однако и при инициализации конкретных классов оно не утрачивает актуальности: часто существует вероятность того, что в процессе разработки/сопровождения программы интерфейс класса изменится (например, конструктор класса станет явным или конструктор копирования станет недоступным) и инициализация копированием перестанет работать."
ОтветитьУдалитьКонструкторы не отосятся к интерфейсам классов, потому что они не описывают поведение.
Кстати именно из таких соображений я не рекомендую в C# использовать ограничения типов в шаблоне интерфейсов вроде new(). Например, public ISomething: where T: new().
Черт, шаблон интерфейса парсер съел. Ну я думаю, все понятно.
УдалитьЯ придерживаюсь общепринятой терминологии С++, когда понятие "интерфейс класса" употребляется и для обозначения "внешней части" класса (т.е. части, которая необходима клиенту для того, чтобы откомпилировать программу). Возможно, лучше назвать это сигнатурой класса, но так не принято выражаться, и придется давать разъяснения (как и в случае использования термина "внешняя часть"). Термин "объявление класса" в С++ означает несколько иное. "Определение класса" может содержать детали реализации класса - тоже не годится. Кстати, Скотт Майерс в своей знаменитой книге (55 советов...) тоже местами использует подобную терминологию.
УдалитьМурад, смирись, наконец, с тем, что одни и те же термины могут означать разные вещи - это присуще большинству языков.
Явный конструктор копирования (например, в примере выше: explicit Entity( const Entity &)) имеет следующий "побочный" эффект: нельзя создать функцию, возвращающую Entity по значению или сгенерировать исключение типа Entity.
ОтветитьУдалить