Как стать автором
Обновить

Комментарии 39

Извините, но компилятор всё делает правильно, это вы где-то запутались.

Что мы тут видим? А видим мы боль. Компилятор проигнорировал тот факт, что конструкторы и деструкторы M имеют глобальные побочные эффекты. Умный компилятор выбросил из нашей логической цепочки:

— создание копии m1 для передачи копии локального объекта как результата работы функции;
— вызов деструктора локального объекта m1 (что логично после выполнения первого пункта).

Деструктор локального объекта был вызван только после вызова конструктора копирования m2/m3.

В итоге — I изменилась не на 10, а на 6.

Ничего компилятор не проигнорировал. В этом случае вообще нет copy elision. Создал временный объект, скопировал его, вызвал деструктор. И так два раза. Итого 6. Деструкторы объектов m2 и m3 вызываются уже при выходе из main(). А вот что действительно вызывает вопрос: почему в вашем выводе не присутствует вывода из этих деструкторов. Корректный результат, который получаю я, должен выглядеть так:

M(100)
M(const M &obj)
~M()

M(-100)
M(const M &obj)
~M()

6
~M()
~M()

В следующем примере у вас вообще undefined behavior, так как вы возвращаете ссылку на временный объект. Обратный порядок вызова копирующего конструктора и деструктора обусловлен тем, что вы не продлеваете время жизни временных объектов, возвращая иx по значению. Они удаляются до выхода из функции.

Copy elision это очень полезная оптимизация, и в 99.999% случаев она обладает не только желаемым, но и вполне очевидным для программиста поведением, а вы её выставляете как какое-то зло и источник проблем.

P.S. Здесь вы забыли вернуть значение:

M &operator=(const M &obj)
    {
        /* Копирующий оператор присваивания */
        i = obj.i;
        cout << "M &operator=(const M &obj)" << endl;
    }
> Ничего компилятор не проигнорировал. В этом случае вообще нет copy elision.

Вообще-то есть, и clang, например, её делает.
Если поменять NRVO на RVO будут делать и другие компиляторы:

M func( int Value )
{
    if ( Value > 0 ) {
        return M{100};
    }
    else {
        return M{-100};
    }
}

И резуьтат работы программы будет несколько отличным от приведенного в статье:

M(100)

M(-100)

2
~M()
~M()

Вывод который сделал автор о работе программы не имеет отношения к copy elision. И на его компиляторе оптимизации не происходило.
Верно, забыл обновить код в процессе тестирования. У меня в программе написано

if (this != &obj)
{

}
return *this;

Исправлю, как приду с работы.
Не забыл, а упустил из вида при написании статьи в 2 часа ночи. Листинг тестируемой программы содержит:

if (this != &obj)
{...}
return *this;

Но дело совсем не в этом недочете листинга. Сам оператор присваивания и не вызывается в программе. Зачаточная версия &operator= присутствует в упрощенном виде, чтобы показать, что оператор = вообще не вызывается. Исправил.
Деструкторы объектов m2 и m3 вызываются уже при выходе из main().

Вызов деструкторов m2 и m3 не имеет ни малейшего отношения к рассматриваемой ситуации. В моем примере вызов деструкторов этих объектов не происходит по той простой причине, что программа "навечно" засыпает при вызове Sleep(-1);
Деструкторы объектов m2 и m3 вызываются уже при выходе из main(). А вот что действительно вызывает вопрос: почему в вашем выводе не присутствует вывода из этих деструкторов. Корректный результат, который получаю я, должен выглядеть так:

Деструкторы вызываются при выходе из программы. У меня выхода из программы нет, возможно вы не заметили Sleep(-1).
Если вы получаете результат, это не значит, что он корректный.
Я не забыл вернуть значение. &operator= описан для галочки, чтобы увидеть, вызывается ли он вообще, пусть и сам для себя. Возможно вы не заметили, что он не вызывается.
Вообще-то в этом случае есть copy elision.
Извините, я должен был навечно зависнуть на этой строке, и не добраться до комментариев.
-fno-elide-constructors в помощь, чтобы исключать рассуждения по типу:
По логике произойти должно следующее:

kirilodius, ведь это флаг для GCC? Не могу найти аналог для VS 2013.
промахнулся с ответом, вот
Благодарю!
В результате чего — программа функционирует абсолютно неправильно, а потенциальное количество проблем в крупном проекте увеличивается до неприличия.

Знаете ли вы реальные примеры, когда эти потенциальные проблемы становились актуальными проблемами? Первые пару лет моего знакомства с C++ я вообще не знал о copy elision, но и в это время и после того, как узнал о copy elision, у меня никогда не возникало с ним проблем (если, конечно, не считать проблемой, что я, иногда, забываю, что он есть).

На самом деле, есть еще несколько интересных моментов, которые вытекают из такого поведения. Тем, кому интересно — рекомендую поиграться с разными настройками компиляции и перемещающими конструкторами.

Мне интересно, я поигрался и ничего интересного не обнаружил. Можно подробнее? Я пользуюсь регулярно двумя компиляторами: g++ и clang++, в них огромное множество различных опций, можно как-то конкретнее? Хотя бы какие опции приводят к интересным эффектам, про сами эти эффекты я уж не спрашиваю.
Понятия не имею, что происходит на g++, clang++ и так далее. В статье ясно написано, что тесты проводились на VS 2013.
А первый вопрос так и остался без ответа… Касательно ответа на второй вопрос, в компиляторах от Microsoft различных опций вряд ли сильно меньше чем в g++/clang++, так что вопрос все еще актуален. Я молчу о том, что можно было бы объяснить, что эти опции делают, а уж с поиском аналогов, если они есть, заинтересованный читатель справится — это точно лучше, чем вставлять в статью ничем не подкрепленные утверждения.
Да, я знаю реальные примеры, когда такое поведение приводит к проблемам.К сожалению — я не могу выложить код, так как в нем 4 миллиона строк и это военная тайна, без шуток.

В общих чертах проблемы возникают в ситуациях, вроде:
A a_val { a2 (f(x) + f(y) a1) — a3a4};
И эта проблема появляется именно из-за пропуска конструктора копирования/перемещения, тк при создании локального объекта происходит обращение к ядру распределенной системы.
Ваше описание ситуации, к сожалению, ничего даже близко не поясняет. Попробуем прочитать этот ваш военный код. Я так понимаю, что a2 это какой-то класс/функция. В f(y) a1, я подозреваю, пропущена запятая. Далее я предполагаю, что объект, который создается в результате "a2" (чтобы это ни было) является числом или чем-то на него похожим.

Но вот чего я из вашего замечательного описания ситуации не понимаю, так это создание каких именно объектов (или правильнее сказать пропуск создания объектов) приводит к проблемам, и уж подавно не понимаю, как это должно быть связано с обращением к ядру распределенной системы. Хотя, наверно, это опять военная тайна?

И да, второй вопрос так и остался без ответа.
Очень плохо, что таких неаккуратных программистов допускают до военной разработки.
Мало того, что вы агрессивно ведёте дискуссию, так ещё и пользуетесь устаревшими компиляторами.
Sleep(-1);

Постоянно приходится переучивать студентов, отбивая эту привычку подвешивать своё приложение в конце работы, потому что "окно быстро закрывается, ничего не видно". А ведь такое приложение не пройдёт тесты, в которых принимается стандартный ввод и печатается стандартный вывод, который затем проверяется на корректность. Даже если вывод правильный — программа-то зависла!

Самое брутальное, что приходилось встречать — это
system("PAUSE.EXE");

На такое я всегда отвечал, что на моём Линуксе pause.exe не установлен.

Мой совет: поройтесь в настройках проекта VS и найдите опцию, которая контролирует (не)закрытие окна после завершения программы. Тогда не придётся вставлять это безобразие в каждую программу.
Мое приложение не создавалось для прохождения тестов. И у меня не линукс.
И, к счастью или несчастью, я не ваш студент. Поэтому не стоит меня учить, что применять и как настраивать мои проекты.
Мое приложение не создавалось для прохождения тестов.

Но именно этому посвящена статья — тестированию поведения приложения.

И у меня не линукс.

Это не повод, чтобы писать намеренно зависающие программы, когда есть простой и правильный способ.
Еще раз повторяю — я не ваш студент. Простых и правильных способов нет по простой причине — задачи, цели и ситуации бывают разные. Навязывать свое мнение другому человеку, когда вас об этом не просят — плохая привычка.
Ответ со Stackoverflow:

Yes, this is copy elision through Named Return Value Optimization.

The C++ standard allows an implementation to omit a copy operation resulting from a return statement, even if the copy constructor has side effects.

Reference:

C++03 Standard:
12.8 Copying class objects:

# 15

When certain criteria are met, an implementation is allowed to omit the copy construction of a class object, even if the copy constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.111) This elision of copy operations is permitted in the following circumstances (which may be combined to eliminate multiple copies):

— in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object with the same cv-unqualified type as the function return type, the copy operation can be omitted by constructing the automatic object directly into the function’s return value

— when a temporary class object that has not been bound to a reference (12.2) would be copied to a class object with the same cv-unqualified type, the copy operation can be omitted by constructing the temporary object directly into the target of the omitted copy
Автор воюет со стандартом. Он не понимает что есть данность, а что — в его власти.
Писать конструктор копирования (и копирующее присваивание) нужно без сайд-эффектов, предполагая возможный copy elision.

По моему личному мнению, это вообще логично: юзер библиотеки, создавая объект копированием, вправе рассчитывать на то, что ничего кроме собственно создания происходить не будет. You must not pay for what you don't use.

Грубо говоря, конструкторы копирования не должны менять ничего снаружи себя. Соблюдайте это правило, и всё будет хорошо.
Не, на самом деле не очень логично. Например, shared pointer должен инкрементить счётчик ссылок на конструкторе копирования и выполнять удаление (если это требуется) на деструкторе… Другой вопрос, что за счёт элизии деструкторов временных объектов (которая, исходя из стандарта тоже происходит), эта проблема не возникает.
Именно. Это внутренний счётчик ссылок shared_ptr. И если copy elision не будет, он просто увеличится и вновь уменьшится после последующего разрушения. А если будет copy elision — то — ну что, ничего не изменится. Сайд-эффекта по сути нет.
Хм. Почитал определение того, что такое side effect (вот тут — первая ссылка в гугл).

Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.

Я так понял, что под side effect подразумевается, в том числе, изменение состояния объекта (modifying an object), в том числе внутреннего состояния. Выходит, что некоторые классы (в том числе shared_ptr) вынуждены нарушать принципы языка.
Ну как такового запрета на сайд-эффекты в конструкторе копирования нет в стандарте. Это просто рекомендация: учитывать возможный copy elision.

Давайте перефразируем: конструктор копирования не может создавать наблюдаемые сайд-эффекты. Что он там делает внутри — неважно. Главное что копирование с copy-elision и без него (в случаях, когда компилятор может сделать copy elision, но его отключили флагами, например) друг от друга не отличаются.
Ну да. Такое себе условие обязательного "балансирования" эффектов конструктора копирования деструктором.
Ну я всё-таки считаю, что это логично.

Конечно, читая этот пост и тут ещё была ссылка, мол, все с опытом 3+ лет на C++ и не знают про copy elision… Ну, ребята, вы даёте...
С ним просто не сразу сталкиваешься так, чтобы заметно было. Достаточно специфические условия проявления — нужно, чтобы были какие-то значимые действия в конструкторе копирования и при этом что-то пошло не так.
Я с ним столкнулся, наверное, почти сразу, когда читал, как же правильно возвращать из функции. (Ну и возвращать надо by-value, а копирования не будет из-за copy elision.)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации