Перемещение локальной переменной в возвращаемое значение другого типа - PullRequest
7 голосов
/ 06 июня 2019

При возврате значений из функции в C ++ у нас есть копия elision и (Named) оптимизация возвращаемых значений, помогающая нам создавать более эффективный код. Короче, следующий код:

std::vector<int> make_vec_1(){
    std::vector<int> v;
    v.resize(1e6);
    return v;
}

приводит к тихому перемещению или прямому построению в месте назначения возвращаемого значения вместо копии. Правила вокруг этого также означают, что явное перемещение возвращаемого объекта при возврате фактически предотвращает эти оптимизации.

std::vector<int> make_vec_2(){
    std::vector<int> v;
    v.resize(1e6);
    return std::move(v); // BAD
}

Эта версия предотвращает RVO, как объяснено в «Скотте Мейерсе» Effective Modern C ++ , Item 25.


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

std::optional<std::vector<int>> make_opt_vec_1(){
    std::vector<int> v;
    v.resize(1e6);
    return v; // no move
}

std::optional<std::vector<int>> make_opt_vec_2(){
    std::vector<int> v;
    v.resize(1e6);
    return std::move(v); // move
}

Что из этого верно? Сначала линия return std::move(v) выглядит как красный флаг, но я также подозреваю, что это правильно. То же самое касается следующих двух функций, возвращающих пару векторов:

std::pair<std::vector<int>, std::vector<int>> make_vec_pair_1(){
    std::vector<int> v1, v2;
    v1.resize(1e6);
    v2.resize(1e6);
    return {v1, v2}; // no move
}

std::pair<std::vector<int>, std::vector<int>> make_vec_pair_2(){
    std::vector<int> v1, v2;
    v1.resize(1e6);
    v2.resize(1e6);
    return {std::move(v1), std::move(v2)}; // move
}

В этом случае, несмотря на то, что на первый взгляд это выглядит странно, я думаю, что лучше перейти на возвращаемое значение.

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

1 Ответ

9 голосов
/ 06 июня 2019

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

Вы упустили одну вещь.Даже если типы различаются, автоматически выполняется неявное перемещение.

[class.copy.elision] (выделение)

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

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

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

Разрешение перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначен как значение .Если первое разрешение перегрузки не удалось или не было выполнено, или если тип первого параметра выбранного конструктора не является rvalue-ссылкой на тип объекта (возможно, cv-квалифицированный), разрешение перегрузки выполняется снова, рассматривая объект какименующий.[Примечание: это двухэтапное разрешение перегрузки должно выполняться независимо от того, будет ли выполнено копирование.Он определяет конструктор, который будет вызван, если elision не выполняется, и выбранный конструктор должен быть доступен, даже если вызов исключен.- конец примечания]

Это не зависит от соответствия типов и является резервным поведением в случае, если полное (N) RVO не происходит.Таким образом, вы ничего не получите, двигаясь явно в make_opt_vec_2.

Учитывая, что std::move является либо пессимизацией, либо полностью излишним, я бы сказал, что лучше не делать это при простом возврателокальный объект функции.

Единственный случай, когда вы хотите явно написать движение, это когда возвращаемое вами выражение является более сложным.В этом случае вы действительно одиноки, и не двигайтесь - это потенциальная пессимизация.Таким образом, в make_vec_pair_2 перемещение в пару является правильным действием.

Основное правило здесь - не перемещать только выражение id, которое является локальным объектом функции.В противном случае отойдите.

...