Предотвращает ли возврат созданного по умолчанию объекта NRVO?

Предположим, у меня есть такая функция, как:

using std::vector;

vector<int> build_vector(int n)
{
   if (some_condition(n)) return {};

   vector<int> out;

   for(int x : something())
   {
      out.push_back(x);
   }

   return out;
}

Предотвращает ли return {} в начале функции NRVO? Мне любопытно, так как кажется, что это было бы эквивалентно следующему:

using std::vector;

vector<int> nrvo_friendly_build_vector(int n)
{
   vector<int> out;

   if (some_condition(n)) return out;

   for(int x : something())
   {
      out.push_back(x);
   }

   return out;
}

Но мне было непонятно, разрешено ли компилятору выполнять NRVO в первом случае.

Вы ищете ответ языкового юриста, цитируя главу и стих из стандарта?

Eljay 17.12.2018 22:37

Правило as-if дает компилятору большую свободу действий. Например, он может превратить первую версию во вторую, поскольку это не меняет наблюдаемого поведения. Тем не менее, вам, вероятно, придется проверить сборку.

NathanOliver 17.12.2018 22:37

но не удалось превратить первое во второе, изменить наблюдаемое поведение, если было исключено копирование ctor с побочным эффектом, которого в противном случае не было бы?

Gordon Bailey 17.12.2018 22:42

@eljay не обязательно, мне больше любопытно, если это то, о чем я должен заботиться на практике

Gordon Bailey 17.12.2018 22:42

Если конструктор вектора имеет побочный эффект, который потенциально может быть обнаружен some_condition, то две версии кода не идентичны. Я не могу придумать какого-либо наблюдаемого побочного эффекта, который может вызвать создание экземпляра вектора в автоматической области видимости. В динамической области, возможно, с вашим собственным распределителем. Но не в автоматическом режиме.

Sam Varshavchik 17.12.2018 22:45

@GordonBailey Если some_condition полагался на это, то, вероятно, это было бы недопустимо. Если этого не произошло, то его можно оптимизировать. Поскольку разрешены RVO / NRVO, компилятор может отказаться от копирования / перемещения, даже если это имеет побочные эффекты.

NathanOliver 17.12.2018 22:50

@GordonBailey Мне действительно было интересно то же самое, (тем более, что копирование не "требуется" стандартом), поэтому я быстро протестировал его в онлайн-компиляторе, заменив std::vector структурой Widget: V1 (build_vector) coliru.stacked-crooked.com/a/5e55efe46bfe32f5 и V2 (nrvo_friendly_build_vector) coliru.stacked-crooked.com/a/51b036c66e993d62. Как вы можете видеть, в этом конкретном случае (struct не видит побочных эффектов от создания some_condition), V1 вызывает конструктор перемещения, если some_condition имеет значение false (по крайней мере, в clang и g ++). HTH

maddouri 17.12.2018 23:16

@ 865719 ох уж интересно! Похоже, что даже с -O3 есть дополнительный вызов ctor и dtor! Можете ли вы дать ответ, чтобы я принял его? coliru.stacked-crooked.com/a/37e9cd5558c16536coliru.stacked-crooked.com/a/b79474463cc8ae44

Gordon Bailey 18.12.2018 15:47
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
8
121
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

От https://en.cppreference.com/w/cpp/language/copy_elision

Under the following circumstances, the compilers are permitted, but not required to omit the copy and move (since C++11) construction of class objects even if the copy/move (since C++11) constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. This is an optimization: even when it takes place and the copy/move (since C++11) constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed:

  • In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a function parameter or a catch clause parameter, and which is of the same class type (ignoring cv-qualification) as the function return type. This variant of copy elision is known as NRVO, "named return value optimization".

  • ...

Ограничений по досрочному возврату нет, поэтому обе версии являются кандидатами в NRVO.

Ответ принят как подходящий

Как просили от ОП, вот адаптированная версия мой комментарий

Мне действительно было интересно то же самое, что и (тем более, что копирование не "требуется" стандартом), поэтому я быстро протестировал его в онлайн-компиляторе, заменив std::vector структурой Widget:

struct Widget
{
    int val = 0;
    Widget()              { printf("default ctor\n"); }
    Widget(const Widget&) { printf("copy ctor\n"); }
    Widget(Widget&&)      { printf("move ctor\n"); }

    Widget& operator=(const Widget&) { printf("copy assign\n"); return *this; }
    Widget& operator=(Widget&&)      { printf("move assign\n"); return *this; }

    ~Widget() { printf("dtor\n"); }

    void method(int)
    {
        printf("method\n");
    }
};

V1 с использованием build_vector(): http://coliru.stacked-crooked.com/a/5e55efe46bfe32f5

#include <cstdio>
#include <array>
#include <cstdlib>

using std::array;

struct Widget
{
    int val = 0;
    Widget()              { printf("default ctor\n"); }
    Widget(const Widget&) { printf("copy ctor\n"); }
    Widget(Widget&&)      { printf("move ctor\n"); }

    Widget& operator=(const Widget&) { printf("copy assign\n"); return *this; }
    Widget& operator=(Widget&&)      { printf("move assign\n"); return *this; }

    ~Widget() { printf("dtor\n"); }

    void method(int)
    {
        printf("method\n");
    }
};

bool some_condition(int x)
{
    return (x % 2) == 0;
}

array<int, 3> something()
{
    return {{1,2,3}};
}

Widget build_vector(int n)
{
   if (some_condition(n)) return {};

   Widget out;

   for(int x : something())
   {
      out.method(x);
   }

   return out;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        return -1;
    }
    const int x = atoi(argv[1]);

    printf("call build_vector\n");
    Widget w = build_vector(x);
    printf("end of call\n");
    return w.val;
}

Выход V1

call build_vector
default ctor
method
method
method
move ctor
dtor
end of call
dtor

V2 с использованием nrvo_friendly_build_vector(): http://coliru.stacked-crooked.com/a/51b036c66e993d62

#include <cstdio>
#include <array>
#include <cstdlib>

using std::array;

struct Widget
{
    int val = 0;
    Widget()              { printf("default ctor\n"); }
    Widget(const Widget&) { printf("copy ctor\n"); }
    Widget(Widget&&)      { printf("move ctor\n"); }

    Widget& operator=(const Widget&) { printf("copy assign\n"); return *this; }
    Widget& operator=(Widget&&)      { printf("move assign\n"); return *this; }

    ~Widget() { printf("dtor\n"); }

    void method(int)
    {
        printf("method\n");
    }
};

bool some_condition(int x)
{
    return (x % 2) == 0;
}

array<int, 3> something()
{
    return {{1,2,3}};
}

Widget nrvo_friendly_build_vector(int n)
{
   Widget out;

   if (some_condition(n)) return out;

   for(int x : something())
   {
      out.method(x);
   }

   return out;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        return -1;
    }
    const int x = atoi(argv[1]);

    printf("call nrvo_friendly_build_vector\n");
    Widget w = nrvo_friendly_build_vector(x);
    printf("end of call\n");
    return w.val;
}

Выход V2

call nrvo_friendly_build_vector
default ctor
method
method
method
end of call
dtor

Как видите, в этом конкретном случае (some_condition не видит побочных эффектов от создания структуры), V1 вызывает конструктор перемещения, если some_condition() ложно (по крайней мере, в clang и gcc, используя -std=c++11 и -O2, в Колиру)

Более того, как и ты заметил, похоже, что то же самое происходит и с -O3.

HTH

ps: Изучая возможность копирования, вы можете найти Журнал Abseil # 11 интересным;)

Вероятно, следует упомянуть, что C++ 17 вводит «гарантированное исключение копирования». Однако мое (базовое) понимание после (очень) быстрого просмотра jonasdevlieghere.com/guaranteed-copy-elision/… и stackoverflow.com/a/38043447/865719 состоит в том, что теперь можно иметь возможность копирования для неподвижных типов (что позволяет возвращать по значению для этих типов), что, если я не ошибаюсь, не имеет отношения к варианту использования OP.

maddouri 18.12.2018 20:43

Другие вопросы по теме