Вопрос по return, c++, copy, object, return-value – Предотвращение копирования объектов с помощью оператора «return»

28

У меня есть очень простой вопрос в C ++. Как избежать копирования при возврате объекта?

Вот пример:

<code>std::vector<unsigned int> test(const unsigned int n)
{
    std::vector<unsigned int> x;
    for (unsigned int i = 0; i < n; ++i) {
        x.push_back(i);
    }
    return x;
}
</code>

Как я понимаю, как работает C ++, эта функция создаст 2 вектора: локальный (x) и копию x, которая будет возвращена. Есть ли способ избежать копирования? (и я не хочу возвращать указатель на объект, но сам объект)

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

«Копия x, которая будет возвращена» может быть построен путем перемещения из x, или его конструкция может стать тем же объектом, что и x. Семантика языка уже избегает любых копий. Mankarse
Вы можете положиться на свой компилятор для выполнения магии NRVO или явно использовать семантику перемещения. Alok Save
Это не обязательно создаст копию. NRVO или симулятор движения могут предотвратить это. Vaughn Cato
переместить семантику:www2.research.att.com/~bs/C++0xFAQ.html#rval chris
В ответ на ваши изменения - вам не нужно менять синтаксис вообще. Все, что имеет право на копирование, должно использовать конструкцию перемещения (если конструкция не полностью исключена). Mankarse

Ваш Ответ

7   ответов
0

Конструктор перемещения гарантированно будет использоваться, если NRVO не происходит

Поэтому, если вы возвращаете объект с помощью конструктора перемещения (например,std::vector) по значению гарантируется, что не будет выполняться полная векторная копия, даже если компилятору не удастся выполнить дополнительную оптимизацию NRVO.

Это упоминается двумя пользователями, которые оказываются влиятельными в самой спецификации C ++:

Джохатан Уэйкли в Гарантируется ли перемещение объекта при его возврате? Говард Хиннант Как вернуть объект из функции с учетом значений C ++ 11 и переместить семантику?

Не удовлетворен моим призывом к знаменитости?

ХОРОШО. Я не могу полностью понять стандарт C ++, но я могу понять примеры, которые он имеет! ; -)

Цитируя C ++ 17 n4659 стандартный черновик 15.8.3 [class.copy.elision] "Копировать / переместить elision"

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

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

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

4 [Пример:

class Thing {
public:
  Thing();
  ~ Thing();
  Thing(Thing&&);
private:
  Thing(const Thing&);
};

Thing f(bool b) {
  Thing t;
  if (b)
    throw t;          // OK: Thing(Thing&&) used (or elided) to throw t
  return t;           // OK: Thing(Thing&&) used (or elided) to return t
}

Thing t2 = f(false);  // OK: no extra copy/move performed, t2 constructed by call to f

struct Weird {
  Weird();
  Weird(Weird&);
};

Weird g() {
  Weird w;
  return w;           // OK: first overload resolution fails, second overload resolution selects Weird(Weird&)
}

- конец примера

Мне не нравится формулировка «может быть использовано», но я думаю, что намерение состоит в том, чтобы означать, что если удерживается «3.1» или «3.2», то должно произойти возврат значения.

Это достаточно ясно для комментариев к коду.

Проход по ссылке +std::vector.resize(0) для нескольких звонков

Если вы делаете несколько звонков наtest, Я считаю, что это было бы немного более эффективно, поскольку это экономит несколькоmalloc() звонки + копии перемещений, когда вектор удваивается в размере:

void test(const unsigned int n, std::vector<int>& x) {
    x.resize(0);
    x.reserve(n);
    for (unsigned int i = 0; i < n; ++i) {
        x.push_back(i);
    }
}

std::vector<int> x;
test(10, x);
test(20, x);
test(10, x);

при условииhttps: //en.cppreference.com/w/cpp/container/vector/resiz говорит:

мкость @Vector никогда не уменьшается при изменении размера на меньший размер, потому что это делает недействительными все итераторы, а не только те, которые будут аннулированы эквивалентной последовательностью вызовов pop_back ().

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

С другой стороны, это:

делает интерфейс более уродливым использует больше памяти, чем необходимо при уменьшении размера вектора

так что есть компромисс.

17

начения (NRVO). Глянь сюда:http: //en.wikipedia.org/wiki/Copy_elisio

В C ++ 11 есть конструкторы перемещения и присваивания, которые также дешевы. Вы можете прочитать учебник здесь:http: //thbecker.net/articles/rvalue_references/section_01.htm

Следует отметить, что не все компиляторы будут делать это, и даже те, которые делают это не всегда. Возможно, все же стоит взглянуть на IFF, поскольку объект большой, вы отмечаете копии, а профилирование показывает, что это является существенным узким местом. Crazy Eddie
14

Оптимизация возвращаемых значений сделает всю работу за вас с компилятор пытается исключить избыточные вызовы конструктора копирования и деструктора во время использования.

    std::vector<unsigned int> x;
    return x;
}
...
std::vector<unsigned int> y;
y = test(10);

с оптимизацией возвращаемого значения:

y созданx созданx назначается на yx разрушен

(если вы хотите попробовать это сами для более глубокого понимания, посмотрите на это мой пример)

или даже лучше, как Матье М. указал, если ты позвонишьtest в той же строке, гдеy объявлено, вы также можете избежать создания избыточного объекта и избыточного назначения x будет построен в памяти, гдеy будет сохранено):

std::vector<unsigned int> y = test(10);

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

ИЛ вы могли бы изменить свой код, чтобы передать ссылку на вектор в вашу функцию, что было бы семантически более правильным, не допуская копирования:

void test(std::vector<unsigned int>& x){
    // use x.size() instead of n
    // do something with x...
}
...
std::vector<unsigned int> y;
test(y);
@ MatthieuM .: Я ценю вашу точку зрения, хотя. Проверьте мой ответ сейчас:) LihO
Я не могу дважды поднять голос: Matthieu M.
Ах я вижу. На самом деле я проигнорировал тот факт, что вы сначала создали стандартное значениеy прежде чем назначить на него. Ваша последовательность событий в этом случае правильная, хотя я бы порекомендовал вам просто инициализироватьy, чтобы избежать создания двух объектов, где одного будет достаточно. Извините за шум. Matthieu M.
38

есть некоторая путаница относительно того, как работает RVO (Оптимизация возвращаемого значения).

Простой пример:

#include <iostream>

struct A {
    int a;
    int b;
    int c;
    int d;
};

A create(int i) {
    A a = {i, i+1, i+2, i+3 };
    std::cout << &a << "\n";
    return a;
}

int main(int argc, char*[]) {
    A a = create(argc);
    std::cout << &a << "\n";
}

И вывод на Ideone:

0xbf928684
0xbf928684

Удивительно?

На самом деле, это эффект RVO: объект подлежит возврату построен напрямуюна мест в звонящем.

Как

Традиционно звонящий main здесь) зарезервирует некоторое место в стеке для возвращаемого значения: возвратный слот; вызываемый create здесь) передается (как-то) адрес возвращаемого слота, в который копируется его возвращаемое значение. Затем вызываемый объект выделяет свое собственное пространство для локальной переменной, в которой он создает результат, как и для любой другой локальной переменной, и затем копирует его в возвращаемый слот после return заявление

RVO запускается, когда компилятор выводит из кода, что переменная может быть встроена непосредственно в возвратный слот с эквивалентной семантикой (правило «как будто»).

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

Когда

Компилятор, скорее всего, будет использовать простые правила, такие как:

// 1. works
A unnamed() { return {1, 2, 3, 4}; }

// 2. works
A unique_named() {
    A a = {1, 2, 3, 4};
    return a;
}

// 3. works
A mixed_unnamed_named(bool b) {
    if (b) { return {1, 2, 3, 4}; }

    A a = {1, 2, 3, 4};
    return a;
}

// 4. does not work
A mixed_named_unnamed(bool b) {
    A a = {1, 2, 3, 4};

    if (b) { return {4, 3, 2, 1}; }

    return a;
}

В последнем случае (4) оптимизация не может быть применена, когдаA возвращается, потому что компилятор не может собратьa в обратном слоте, так как это может понадобиться для чего-то другого (в зависимости от логического условияb).

Простое правило таково:

RVO следует применять, если до @ не было объявлено ни одного другого кандидата на место возвратreturn заявление

В случае (4), это зависит от компилятора (насколько он умен) и специфики кода. Например, с конкретным кодом, который вы показываете, умный компилятор может заметить, что инициализацияa не имеет побочных эффектов, и что это объявление может быть перемещено вниз подif. Cheers and hth. - Alf
@ MatthieuM. Это все еще работает, если выдается исключение? naab
+ 1 за указание на то, что мы можем избежать не только копирования, но и создания избыточного объекта и избыточного назначения;) LihO
@ naab: Я не совсем уверен, что вижу проблему? Все локальные переменные до возникновения исключения должны быть уничтожены, независимо от того, где они находятся (в функциональном фрейме или в функциональном фрейме вызывающего). Поэтому я не понимаю, почему броски будут иметь влияние. Matthieu M.
@ Cheersandhth.-Alf: Точно, правило «как будто» все еще очевидно. В общем случае, хотя (конструктор вне линии), это будет вычитаться только с включенным LTO. Matthieu M.
2

о как оптимизация возвратной стоимости). Видетьhttps: //isocpp.org/wiki/faq/ctors#return-by-value-optimizatio

-5

вы можете объявить ваш тип возвращаемого значения как std :: vector, и в этом случае вместо копии будет возвращена ссылка.

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

Наконец, многие компиляторы C ++ могут оптимизировать возвращаемое значение (http://en.wikipedia.org/wiki/Return_value_optimization), исключая временный объект в некоторых случаях.

Очень жаль, что ссылка будет немедленно запрещена для использования (UB). -1 за дурной совет. Mankarse
1

Ссылка будет работать.

Void(vector<> &x) {

}

Похожие вопросы