Автоматически определять C++14 "возврат должен использовать 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.
1 ответ
Правильное поведение - перемещение / копирование. Вы, вероятно, хотите просто написать чек-опрятный чек.
Формулировка в 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-квалифицированный), разрешение перегрузки выполняется снова, рассматривая объект как именующий.
В:
Unrelated test1() {
Derived d1;
return d1;
}
Мы встречаем первую пулю, поэтому мы пытаемся скопировать-инициализировать Unrelated
со значением типа Derived
, который дает нам Unrelated(Derived&& )
, Это соответствует выделенным критериям, поэтому мы используем его, и результатом является ход.
В:
Base test2() {
Derived d2;
return d2; // yes, this is slicing!
}
Мы снова встретим первую пулю, но разрешение перегрузки найдем Base(Base&& )
, Первый параметр выбранного конструктора не является ссылкой на значение Derived
(возможно, cv-qualified), поэтому мы снова выполняем разрешение перегрузки и заканчиваем копированием.