Является ли этот код четко определенным, независимо от того, есть ли копия?

Рассмотрим этот код:

#include <iostream>

struct Test
{
    int x;
    int y;
};

Test func(const Test& in)
{
    Test out;
    out.x=in.y;
    out.y=in.x;
    return out;
}

int main()
{
    Test test{1,2};
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
    test=func(test);
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
}

Можно было бы ожидать такого вывода:

x: 1, y: 2
x: 2, y: 1

и это действительно то, что я получаю. Но из-за исключения копирования, может ли out находиться в том же месте памяти, что и in, и в результате последняя строка вывода будет x: 2, y: 2?

Я пробовал скомпилировать с помощью gcc и clang с -O0 и -O3, но результаты по-прежнему выглядят так, как задумано.


person Ruslan    schedule 23.10.2015    source источник


Ответы (5)


Это хорошо сформированный код, оптимизация не может сломать хорошо сформированный код, поскольку это нарушит как если бы правило. Это говорит нам о том, что:

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

за исключением копии:

[...] реализации разрешается опускать конструкцию копирования / перемещения объекта класса, даже если конструктор, выбранный для операции копирования / перемещения, и / или деструктор для объекта имеют побочные эффекты. [...]

Но правила последовательности по-прежнему должны соблюдаться, и если мы перейдем к черновику стандарта, мы увидим, что знаем, что присваивание выполняется после того, как левый и правый операнды оцениваются из раздела 5.17:

Во всех случаях присваивание выполняется после вычисления значения правого и левого операндов и перед вычислением значения выражения присваивания.

и мы знаем, что тело функции неопределенно упорядочено по отношению к другим вычислениям, которые специально не упорядочены с вызовом функции из раздела 1.9:

Каждая оценка в вызывающей функции (включая вызовы других функций), которая иначе не упорядочена до или после выполнения тела вызываемой функции, имеет неопределенную последовательность относительно выполнения вызываемой функции 9.

и неопределенная последовательность означает:

Оценки A и B являются неопределенными последовательностями, когда либо A упорядочивается до B, либо B - до A, но не указано, что именно. [Примечание: оценки с неопределенной последовательностью не могут перекрываться, но любая из них может быть выполнена первой. —В конце примечания]

person Shafik Yaghmour    schedule 23.10.2015

Нет, не может. Оптимизация не может сломать правильно сформированный код, и этот код правильно сформирован.

РЕДАКТИРОВАТЬ: небольшое обновление. Конечно, мой ответ предполагает, что сам компилятор не содержит ошибок, о чем, конечно же, можно только молиться :)

EDIT2: некоторые люди говорят о побочных эффектах в конструкторах копирования и о том, что они плохие. Конечно, неплохие. Мне кажется, что в C ++ не гарантируется создание известного количества временных объектов. Вам гарантируется, что каждый созданный временный объект будет уничтожен. Хотя оптимизации позволяют уменьшить количество временных объектов, выполняя копирование, им также разрешено увеличивать его! :) Пока ваши побочные эффекты кодируются с учетом этого факта, вы в порядке.

person SergeyA    schedule 23.10.2015
comment
Разрешено ли стандартом явно делать дополнительные копии, чтобы это привело к неожиданным дополнительным побочным эффектам от конструктора копирования? Для исключения копий действительно есть особое исключение, но для умножения копий я не слышал ни о каком. - person Ruslan; 23.10.2015
comment
Я не стандартный гуру, поэтому я не уверен, есть ли что-то, что может быть истолковано так, чтобы компиляторы не добавляли дополнительные копии. - person SergeyA; 23.10.2015

Нет, не могло!

Оптимизация не означает, что вы получаете неопределенное поведение в хорошо написанном (не плохо обусловленном) коде.

Проверьте это исх .:

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

Соответствующая реализация, выполняющая правильно сформированную программу, должна производить такое же наблюдаемое поведение, как одна из возможных последовательностей выполнения соответствующего экземпляра абстрактной машины с той же программой и теми же входными данными. ...

Наблюдаемое поведение абстрактной машины - это последовательность операций чтения и записи в изменчивые данные и вызовов библиотечных функций ввода-вывода. ...

взято из этого ответа.

В этом ответе вы можете увидеть случай, когда copy-elision может давать другой результат!

person gsamaras    schedule 23.10.2015

Единственное, что разрешено "нарушить" copy elision, - это когда у вас есть побочные эффекты в вашем конструкторе копирования. Это не проблема, потому что конструкторы копирования всегда не должны иметь побочных эффектов.

Для иллюстрации вот конструктор копирования с побочными эффектами. Поведение этой программы действительно зависит от оптимизаций компилятора, то есть от того, действительно ли сделана копия или нет:

#include <iostream>

int global = 0;

struct Test
{
    int x;
    int y;

    Test() : x(0), y(0) {}

    Test(Test const& other) :
        x(other.x),
        y(other.y)
    {
        global = 1; // side effect in a copy constructor, very bad!
    }
};

Test func(const Test& in)
{
    Test out;
    out.x=in.y;
    out.y=in.x;
    return out;
}

int main()
{
    Test test;
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
    test=func(test);
    std::cout << "x: " << test.x << ", y: " << test.y << "\n";
    std::cout << global << "\n"; // output depends on optimisation
}

Показанный вами код не имеет таких побочных эффектов, а поведение вашей программы четко определено.

person Christian Hackl    schedule 23.10.2015
comment
Очень хороший ответ, так как он использует тот, на который я ссылался! - person gsamaras; 23.10.2015
comment
Что за чушь! 'побочный эффект в конструкторе копирования, очень плохо!' - просто очень плохо назвал весь класс shared_ptr ... - person SergeyA; 23.10.2015
comment
@SergeyA: Так почему это ерунда и какое отношение это имеет к shared_ptr? - person Christian Hackl; 23.10.2015
comment
Потому что копировщики shared_ptr, конечно, имеют побочные эффекты. - person SergeyA; 23.10.2015
comment
@SergeyA: Если вы имеете в виду увеличение количества владельцев, это не совсем побочный эффект, но относится к основной функциональности конструктора копирования. Когда копия shared_ptr опущена, копия не создается и счетчик не увеличивается. - person Christian Hackl; 23.10.2015
comment
Голову нужно выпрямить. Увеличение счетчика - это побочный эффект конструкции копирования и семантически не отличается от вашего примера. Счетчик даже не принадлежит строящемуся объекту! Итог - вы ошибаетесь по существу. В создании копий нет ничего плохого в побочных эффектах. - person SergeyA; 23.10.2015
comment
@SergeyA: Скажем так: зависимость от побочных эффектов конструктора копирования - это плохо, потому что вы никогда не можете предположить, что копия действительно имела место. Например, в опубликованном мною коде использование global == 1 в качестве индикатора, возвращаемого func, будет ошибкой. P.S .: Вам следует воздержаться от этого враждебного тона и менее эмоционально относиться к техническим разногласиям. - person Christian Hackl; 23.10.2015
comment
Правильно сказать, как я выразился - у вас нет гарантии, что определенное количество временных объектов будет создано в любой момент. Любая зависимость от числа - признак неверно сформированного кода. Не нужно даже упоминать о побочных эффектах. Что касается моего тона, я считаю его безупречным. - person SergeyA; 23.10.2015
comment
@SergeyA Конструктор копирования для shared_ptr в библиотеке GNU ISO C ++ определен как shared_ptr(const shared_ptr&) noexcept = default;. Я с уверенностью могу сказать, что это не имеет побочных эффектов. - person Captain Giraffe; 23.10.2015
comment
@CaptainGiraffe, каково ваше определение побочного эффекта? - person SergeyA; 23.10.2015
comment
@SergeyA Не меняет никаких состояний, кроме самой конструкции. Если вы считаете, что CCTOR shared_ptr имеет побочные эффекты, вы не можете написать какой-либо конструктор без побочных эффектов, и этот термин станет спорным. - person Captain Giraffe; 23.10.2015
comment
@CaptainGiraffe, что такое «любое состояние» и что такое «фактическое строительство»? Если во время построения объекта A я перехожу к какому-то объекту B и что-то здесь меняю, это побочный эффект? - person SergeyA; 24.10.2015
comment
@SergeyA Конечно, objB->nonConst() - это побочный эффект. Вы заявили, что рассматриваемый конструктор копирования имеет побочные эффекты. Что это за побочный эффект? - person Captain Giraffe; 24.10.2015
comment
@CaptainGiraffe, понял. Это означает, что конструктор-копия для shared_ptr с псевдонимом имеет побочные эффекты. Это плохо? - person SergeyA; 24.10.2015

Elision - это слияние сущностей и идентичностей объектов.

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

По сути, Elision ездит на работу. (Если объекты A и B исключены вместе, а объекты B и C исключены вместе, то фактически A и C удаляются вместе).

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

Этот конструктор внешней переменной располагается после тела func и, следовательно, после вызова func. Так что это не могло произойти до вызова func.

Здесь приведен пример случая, когда мы назвали переменную до ее создания, и передать его func, затем инициализировать переменную возвращаемым значением func. Похоже, что компилятор решил не исключать в этом случае, но, как показано ниже в комментариях, id действительно сделал: мой вызов UB скрыл исключение. (свертка была попыткой помешать компилятору предварительно вычислить значения test.x и test.y).

person Yakk - Adam Nevraumont    schedule 23.10.2015
comment
Интересно. В вашем примере func получает одинаковые адреса для in и возвращаемое значение при инициализации test, но его код внимательно читает in перед тем, как коснуться соответствующей части out, таким образом ведя себя так, как если бы in и out были разными переменными. - person Ruslan; 24.10.2015
comment
@ruslan классический UB: он ведет себя правильно для правильной программы, а мой код - нет. - person Yakk - Adam Nevraumont; 24.10.2015