Перемещение семантики в C ++ - Перемещение-возврат локальных переменных

10

Насколько я понимаю, в C ++ 11, когда вы возвращаете локальную переменную из функции по значению, компилятору разрешается обрабатывать эту переменную как ссылку на r-значение и «перемещать» ее из функции для ее возврата (если RVO / NRVO не происходит вместо этого, конечно).

Мой вопрос, не может ли это сломать существующий код?

Рассмотрим следующий код:

#include <iostream>
#include <string>

struct bar
{
  bar(const std::string& str) : _str(str) {}
  bar(const bar&) = delete;
  bar(bar&& other) : _str(std::move(other._str)) {other._str = "Stolen";}
  void print() {std::cout << _str << std::endl;}

  std::string _str;
};

struct foo
{
  foo(bar& b) : _b(b) {}
  ~foo() {_b.print();}

  bar& _b;
};

bar foobar()
{
  bar b("Hello, World!");
  foo f(b);

  return std::move(b);
}

int main()
{
  foobar();
  return EXIT_SUCCESS;
}

Я думал, что деструктор локального объекта мог бы ссылаться на объект, который неявно перемещается, и, следовательно, неожиданно видеть «пустой» объект. Я пытался проверить это (см. Http://ideone.com/ZURoeT ), но я получил «правильный» результат без явного std::moveв foobar(). Я предполагаю, что это из-за NRVO, но я не пытался изменить код, чтобы отключить это.

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

ОБНОВЛЕНИЕ Вот пример, который иллюстрирует то, о чем я говорю. Следующие две ссылки относятся к одному и тому же коду. http://ideone.com/4GFIRu - C ++ 03 http://ideone.com/FcL2Xj - C ++ 11

Если вы посмотрите на вывод, он другой.

Итак, я полагаю, что теперь возникает вопрос, учитывался ли этот вопрос при добавлении неявного перемещения в стандарт, и было решено, что можно было бы добавить это критическое изменение, так как этот вид кода достаточно редок? Мне также интересно, предупредит ли какой-либо компилятор в таких случаях ...

Bwmat
источник
Движение должно всегда оставлять объект в разрушаемом состоянии.
Зан Рысь
Да, но это не вопрос. Код до c ++ 11 мог предполагать, что значение локальной переменной не изменится просто из-за его возврата, поэтому этот неявный шаг может нарушить это предположение.
Bwmat
Вот что я пытался объяснить в своем примере; с помощью деструкторов вы можете проверить состояние (подмножество) локальных переменных функции «после» выполнения оператора return, но до того, как функция фактически вернется.
Bwmat
Это отличный вопрос с примером, который вы добавили. Я надеюсь, что это получит больше ответов от профессионалов, которые могут объяснить это. Единственная реальная обратная связь, которую я могу дать: вот почему у объектов обычно не должно быть несобственных представлений о данных. На самом деле существует множество способов написания невинно выглядящего кода, который вызывает ошибки, когда вы предоставляете объектам несобственные представления (необработанные указатели или ссылки). Я могу уточнить это в правильном ответе, если хотите, но я предполагаю, что это не то, о чем вы действительно хотите услышать. И, кстати, уже известно, что 11 может сломать существующий код, например, путем определения новых ключевых слов.
Нир Фридман,
Да, я знаю, что C ++ 11 никогда не претендовал на то, чтобы не ломать старый код, но это довольно тонко, и его было бы очень легко пропустить (без ошибок компилятора, предупреждений, сбоев)
Bwmat

Ответы:

8

Скотт Мейерс написал в comp.lang.c ++ (август 2010 г.) о проблеме, при которой неявная генерация конструкторов перемещения может нарушить инварианты класса C ++ 03:

struct X
{
  // invariant: v.size() == 5
  X() : v(5) {}

  ~X() { std::cout << v[0] << std::endl; }

private:    
  std::vector<int> v;
};

int main()
{
    std::vector<X> y;
    y.push_back(X()); // X() rvalue: copied in C++03, moved in C++0x
}

Здесь проблема в том, что в C ++ 03 Xбыл инвариант, чтобы его vчлен всегда имел 5 элементов. X::~X()рассчитывал на этот инвариант, но недавно введенный конструктор перемещения переместился из v, тем самым установив его длину на ноль.

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

C ++ 11 пытается достичь баланса между нарушением части существующего кода и предоставлением полезных оптимизаций на основе конструкторов перемещения.

Первоначально Комитет решил, что конструкторы перемещения и операторы присваивания перемещения должны генерироваться компилятором, если он не предоставлен пользователем.

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

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

В N3174 - двигаться или не двигаться Stroupstrup говорит:

Я считаю, что это проблема проектирования языка, а не простая проблема обратной совместимости. Легко избежать взлома старого кода (например, просто удалить операции перемещения из C ++ 0x), но я вижу, как сделать C ++ 0x лучшим языком, сделав операции перемещения распространенными, главная цель, для которой, возможно, стоит сломать некоторый C + +98 код

Manlio
источник