Move Constructor vs Copy Elision. Какой из них вызывается?

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

  • В первом случае функция, возвращающая временное значение, вызывает конструктор перемещения.
  • Во втором случае функция, возвращающая временное значение, просто говорит компилятору выполнить исключение копирования.

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

Обратите внимание, что в следующих примерах оператор‹‹ был перегружен для печати списка (в первом случае) и элемента данных типа double (во втором случае).


КОНСТРУКТОР ПЕРЕМЕЩЕНИЯ ВЫЗЫВАЕТСЯ

template<typename T>
class GList
{
public:
    GList() : il{ nullptr } {}

    GList(const T& val) : il{ new Link<T>{ val,nullptr } }  {}

    GList(const GList<T>& copy) {}

    GList(GList<T>&& move)
    {
        std::cout << "[List] Move constructor called" << std::endl;

        // ... code ...
    }

    // HERE IS THE FUNCTION WHICH RETURNS A TEMPORARY!
    GList<T> Reverse()
    {
        GList<T> result;

        if (result.il == nullptr)
            return *this;

        ...
        ...
        ...

        return result;
    }
};

int main()
{

   GList<int> mylist(1);

   mylist.push_head(0);

   cout << mylist.Reverse();

   return 0;
}

Результат:

[Список] Вызван конструктор перемещения

0

1


КОПИРОВАНИЕ ELISION ВЫПОЛНЕНО

class Notemplate
{
   double d;
public:
   Notemplate(double val)
   {
      d = val;
   }

   Notemplate(Notemplate&& move)
   {
       cout << "Move Constructor" << endl;
   }

   Notemplate(const Notemplate& copy)
   {
       cout << "Copy" << endl;
   }

   Notemplate Redouble()
   {
       Notemplate example{ d*2 };
       return example;
   }
};

int main()
{
   Notemplate my{3.14};

   cout << my.Redouble();

   return 0;
}

Результат:

6.28


Я ожидал вызова конструктора перемещения во втором примере. В конце концов, логика функции та же: вернуть временное значение.

Кто-нибудь объяснит мне, почему этого не происходит?

Как бороться с копированием?

Я хочу, чтобы мой код был максимально переносимым, как я могу быть уверен в такой оптимизации компилятором?


person gedamial    schedule 19.02.2016    source источник
comment
Какая подпись у двух operator<<?   -  person songyuanyao    schedule 19.02.2016
comment
друг std::ostream& оператор‹‹(std::ostream& s, const ClassName& другое)   -  person gedamial    schedule 19.02.2016


Ответы (2)


В комментариях к другому ответу SO ОП поясняет, что он спрашивает здесь:

Я слышал, что исключение копирования МОЖЕТ произойти, даже если имеется более 1 оператора возврата. Я хотел бы знать, когда запрещено копирование

И поэтому я пытаюсь решить эту проблему здесь:

Исключение операций копирования/перемещения (называемое исключением копирования в стандарте C++) разрешено в следующих случаях:

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

  • В throw-expression, когда операнд является именем энергонезависимого автоматического объекта (кроме функции или параметра предложения catch), область действия которого не выходит за пределы самого внутреннего охватывающего < em>try-block (если он есть), операция копирования/перемещения из операнда в объект исключения может быть опущена путем создания автоматического объекта непосредственно в объекте исключения.

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

  • Когда объявление-исключения обработчика исключений объявляет объект того же типа (за исключением квалификации cv), что и объект исключения, операцию копирования можно опустить, обработав объявление-объявления-исключения в качестве псевдонима для объекта-исключения, если смысл программы не изменится, за исключением выполнения конструкторов и деструкторов для объекта, объявленного объявлением-исключением. Не может быть перемещения из объекта исключения, потому что это всегда lvalue.

Копирование запрещено во всех других случаях.

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

Обновление C++17

Теперь есть несколько мест, где удаление копии является обязательным. Если значение prvalue может быть напрямую привязано к параметру функции по значению, или возвращаемому типу по значению, или к именованной локальной переменной, исключение копирования является обязательным в C++17. Это означает, что компилятор не должен беспокоиться даже о проверке конструктора копирования или перемещения. Юридический С++ 17:

struct X
{
    X() = default;
    X(const X&) = delete;
    X& operator=(const X&) = delete;
};

X
foo(X)
{
    return X{};
}

int
main()
{
    X x = foo(X{});
}
person Howard Hinnant    schedule 18.03.2016
comment
Спасибо, профессор :) Есть ли способ сделать мой код максимально переносимым? Способ быть независимым от выбора компилятора - person gedamial; 19.03.2016
comment
@gedamial: Да. Не добавляйте побочные эффекты в свои специальные элементы (конструкторы копирования/перемещения), которые не относятся к копированию/перемещению ваших объектов (за исключением, возможно, образовательных или отладочных целей). Примерами таких побочных эффектов являются операторы печати (как в вашем вопросе) или счетчики (подсчет количества копий или ходов). Если у вас есть такие побочные эффекты, убедитесь, что правильность вашей программы не зависит от этих побочных эффектов, так что если и когда произойдет исключение копии, правильность вашей программы не пострадает. - person Howard Hinnant; 19.03.2016
comment
Большое спасибо, сэр. ты был потрясающим - person gedamial; 19.03.2016

Исключение копирования — это оптимизация, которую в настоящее время обеспечивает каждый современный компилятор.

При возврате огромных объектов класса в C++ этот метод применим... но не во всех случаях!

В первом примере компилятор выполняет конструктор перемещения, потому что у нас есть более одного оператора return в функции.

person gedamial    schedule 20.02.2016