Для автоматического определения C ++ 14 «return следует использовать std :: move» ситуация

Насколько я понимаю, в C ++ 17 следующий фрагмент предназначен для выполнения правильных действий:

struct Instrument;  // instrumented (non-trivial) move and copy operations

struct Base {
Instrument i;
};

struct Derived : public Base {};

struct Unrelated {
Instrument i;
Unrelated(const Derived& d): i(d.i) {}
Unrelated(Derived&& d): i(std::move(d.i)) {}
};

Unrelated test1() {
Derived d1;
return d1;
}

Base test2() {
Derived d2;
return d2;  // yes, this is slicing!
}

То есть в C ++ 17 компилятор должен обрабатывать как d1 а также d2 в качестве значений в целях разрешения перегрузки в этих двух операторах возврата. Однако в C ++ 14 и более ранних версиях это было не так; преобразование lvalue-to-rvalue в return операнды должны были применяться только тогда, когда операнд именно так правильный тип возврата.

Кроме того, GCC и Clang, похоже, ведут себя странно и, возможно, с ошибками в этой области. Пробуя приведенный выше код на Wandbox, Я вижу эти выводы:

GCC 4.9.3 and earlier: copy/copy (regardless of -std=)
Clang 3.8.1 and earlier: copy/copy (regardless of -std=)
Clang 3.9.1 and later: move/copy (regardless of -std=)
GCC 5.1.0 through 7.1.0: move/copy (regardless of -std=)
GCC 8.0.1 (HEAD): move/move (regardless of -std=)

Так что это началось как инструментальный вопрос и закончился с побочным порядком «что, черт возьми, это правильный поведение для компилятора C ++? «

Мой инструментальный вопрос: в нашей кодовой базе у нас есть несколько мест, которые говорят return x; но это случайно производит копию вместо перемещения, потому что наша цепочка инструментов — GCC 4.9.x и / или Clang. Мы хотели бы обнаружить эту ситуацию автоматически и вставить std::move() по мере необходимости. Есть ли простой способ обнаружить эту проблему? Может быть, чек или аккуратный чек -Wfoo флаг мы могли бы включить?

Но, конечно, теперь я также хотел бы знать, что является правильный поведение компилятора C ++ в этом коде. Эти выходы указывают на ошибки GCC / Clang? Над ними работают? И это языковая версия (-std=) должен иметь значение? (Я думаю, что это должно иметь значение, если правильное поведение не было обновлено с помощью отчетов о дефектах вплоть до C ++ 11.)

Вот более полный тест вдохновлен ответом Барри. Мы тестируем шесть различных случаев, когда желательно преобразование lvalue в rvalue.

GCC 4.9.3 and earlier:   elided/copy/copy/copy/copy/copy
Clang 3.8.1 and earlier: elided/copy/copy/copy/copy/copy
Clang 3.9.1 and later:   elided/copy/move/copy/copy/copy
GCC 5.1.0 through 7.1.0: elided/copy/move/move/move/move
GCC 8.0.1 (HEAD):        elided/move/move/move/move/move

ICC 17:                  elided/copy/copy/copy/copy/copy
ICC 18:                  elided/move/move/move/copy/copy
MSVC 2017 (wow):         elided/copy/move/copy/copymove/copymove

После ответа Барри мне кажется, что Clang 3.9+ делает технически правильную вещь во всех случаях; GCC 8+ делает желательный вещь во всех случаях; и вообще я должен перестать учить, что люди «просто return x и пусть компилятор «DTRT» (или, по крайней мере, научит его громадному предупреждению), потому что на практике компилятор не DTRT, если вы не используете передовой (и технически не соответствующий) GCC.

8

Решение

Правильное поведение — перемещение / копирование. Вы, вероятно, хотите просто написать чек-опрятный чек.


Формулировка в C ++ 17 [Class.copy.elision] / 3 а в C ++ 14 есть [Class.copy] / 32. Конкретные слова и форматирование разные, но правило одинаковое.

В C ++ 11 формулировка правила была [Class.copy] / 32 и был привязан к правилам копирования, исключение для локальных переменных автоматического хранения было добавлено в CWG 1579 как сообщение о дефекте. Компиляторы, предшествующие этому отчету о дефектах, будут вести себя как копирование / копирование. Но поскольку отчет о дефектах не соответствует C ++ 11, компиляторы, реализующие изменение формулировки, будут реализовывать его во всех стандартных версиях.

Используя формулировку C ++ 17:

В следующих контекстах инициализации копирования вместо операции копирования может использоваться операция перемещения:

  • Если выражение в операторе возврата является (возможно, заключенным в скобки) идентификатором-идентификатором, который именует объект с автоматическим сроком хранения, объявленным в теле или в параметре-объявлении-условия самой внутренней функции или лямбда-выражения, или
  • […]

Разрешение перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначен значением r. Если первое разрешение перегрузки не удалось или не было выполнено, или если тип первого параметра выбранного конструктора не является rvalue-ссылкой на тип объекта (возможно, cv-qualified), Разрешение перегрузки выполняется снова, рассматривая объект как lvalue.

В:

Unrelated test1() {
Derived d1;
return d1;
}

Мы встречаем первую пулю, поэтому мы пытаемся скопировать-инициализировать Unrelated со значением типа Derived, который дает нам Unrelated(Derived&& ), Это соответствует выделенным критериям, поэтому мы используем его, и результатом является ход.

В:

Base test2() {
Derived d2;
return d2;  // yes, this is slicing!
}

Мы снова встретим первую пулю, но разрешение перегрузки найдем Base(Base&& ), Первый параметр выбранного конструктора не Rvalue ссылка на Derived (возможно, cv-qualified), поэтому мы снова выполняем разрешение перегрузки и заканчиваем копированием.

10

Другие решения

Других решений пока нет …