9

Этот вопрос был вызван путаницей относительно RVO в C++ 11.Какой метод «возврата» лучше для больших данных в C++/C++ 11?

У меня есть два пути к значению «возврата»: возврата по значению и возврата через ссылочный параметр. Если я не считаю производительность, я предпочитаю первый. Поскольку возврат по значению является более естественным, и я могу легко отличить вход и выход. Но, если я считаю эффективность при возврате больших данных. Я не могу решить, потому что в C++ 11 есть RVO.

Вот мой пример кода, эти два кода выполняют ту же работу:

возврат по значению

struct SolutionType 
{ 
    vector<double> X; 
    vector<double> Y; 
    SolutionType(int N) : X(N),Y(N) { } 
}; 

SolutionType firstReturnMethod(const double input1, 
           const double input2); 
{ 
    // Some work is here 

    SolutionType tmp_solution(N); 
    // since the name is too long, I make alias. 
    vector<double> &x = tmp_solution.X; 
    vector<double> &y = tmp_solution.Y; 

    for (...) 
    { 
    // some operation about x and y 
    // after that these two vectors become very large 
    } 

    return tmp_solution; 
} 

возвращение через ссылочный параметр

void secondReturnMethod(SolutionType& solution, 
         const double input1, 
         const double input2); 
{ 
    // Some work is here   

    // since the name is too long, I make alias. 
    vector<double> &x = solution.X; 
    vector<double> &y = solution.Y; 

    for (...) 
    { 
    // some operation about x and y 
    // after that these two vectors become very large 
    } 
} 

Вот мои вопросы:

  1. Как можно обеспечить, что RVO произошло в C++ 11?
  2. Если мы уверены, что произошло RVO, в настоящее время программирование на С ++, в каком методе «возврат» вы используете , рекомендуем? Зачем?
  3. Почему существует некоторая библиотека, использующая возврат через ссылочный параметр, стиль кода или историческую причину?

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

Вот некоторые полезные ссылки по теме, которые помогают мне понять эту проблему:

  1. How to return large data efficiently in C++11
  2. In C++, is it still bad practice to return a vector from a function?
  3. Want Speed? Pass by Value.

ответ

16

Прежде всего, правильный технический термин для того, что вы это NRVO. РВО относится к временные конструкции возвращается:

X foo() { 
    return make_x(); 
} 

NRVO относится к именованные объекты возвращаются:

X foo() { 
    X x = make_x(); 
    x.do_stuff(); 
    return x; 
} 

Во-вторых, (N) РВО является оптимизация компилятора и не является обязательным. Однако вы можете быть уверены, что если вы используете современный компилятор, (N) RVO будут использоваться довольно агрессивно.

В-третьих, (N) РВО является не C++ 11 особенность - она ​​была здесь задолго до 2011

Forth всего, что вы имеете в C++ 11 представляет собой шаг конструктор.Поэтому, если ваш класс поддерживает перемещение семантики, он будет перемещен из, а не скопирован, даже если (N) RVO не происходит. К несчастью, не все может быть семантически перемещено эффективно.

В-пятых, возврат по ссылке - ужасный антипаттерн. Он гарантирует, что объект будет эффективно создан дважды - первый раз как «пустой» объект, второй раз, когда он заполняется данными, - и это не позволяет вам использовать объекты, для которых «пустое» состояние не является допустимым инвариантом.

+0

Будет ли он быть перемещен, даже если в 'возвратных х;' 'x' является Левым и' станд :: move' отсутствует? – Zereges

+7

Не большой поклонник * четвертого *, а? – IInspectable

+4

@Zereges Никогда не возвращайтесь с помощью 'return std :: move (что-то);' Это пессимизация. – NathanOliver

1

Невозможно гарантировать, что RVO (или NVRO) встречается в C++ 11. Независимо от того, происходит это или нет, это связано с качеством реализации (например, компилятора), а не с чем-то, что в корне контролируется программистом.

Перемещение семантики может использоваться в некоторых случаях для достижения аналогичного эффекта, но отличается от RVO.

Как правило, я рекомендую использовать любой метод возврата для данных под рукой, что понятно программисту. Код, который программист может понять, легче получить правильно. Переключение с использованием тайных методов для оптимизации производительности (например, попытка заставить NVRO) имеет смысл сделать код более трудным для понимания, чем, следовательно, более вероятно, что будут иметь ошибки (например, увеличенный потенциал для неопределенного поведения). Если код работает правильно, но MEASUREMENTS показывают, что ему не хватает требуемой производительности, то для повышения производительности можно изучить более сложные методы. Но попытка с любовью вручную оптимизировать код вперед (т. Е. До того, как какие-либо измерения предоставили доказательства необходимости), называется «преждевременная оптимизация» по какой-либо причине.

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

EDIT: добавление примера, в котором возвращение по ссылке является опасным, как указано в комментарии.

AnyType &func() 
    { 
     Anytype x; 
     // initialise x in some way 

     return x; 
    }; 

    int main() 
    { 
     // assume AnyType can be sent to an ostream this wah 

     std::cout << func() << '\n';  // undefined behaviour here 
    } 

В этом случае func() возвращает ссылку на то, что больше не существует после того, как он возвращается - обычно называют оборванных ссылки. Поэтому любое использование этой ссылки (в данном случае для печати указанного значения) имеет неопределенное поведение. Возврат по значению (т. Е. Просто удаление &) возвращает копию переменной, которая существует, когда вызывающий объект пытается ее использовать.

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

+0

Спасибо за ваш ответ. я знаю это, «преждевременная оптимизация - это корень всего зла». И я не пытаюсь его оптимизировать. Я новичок в C++, когда я пишу код, я просто хочу написать его правильно. Можете ли вы привести мне пример «возвращение ссылки на что-то опасное»? Я думаю, что ссылка находится в списке параметров, она ** существует **. – Regis

+0

Хорошо. Я привел пример функции, возвращающей оборванную ссылку.Немного сложнее, но все же легко, построить аналогичные примеры, где аргумент передается по ссылке, и эта ссылка возвращается, но упомянутый объект перестает существовать до его использования. – Peter

+0

Извините, возможно, я не стал четко охарактеризовать вопрос. В моем вопросе он не возвращается по ссылке, он «возвращается» через ссылочный параметр, а реальный возврат недействителен. Моя притязательность для введения в заблуждение. – Regis

3

Ответ SergyA является совершенным. Если вы последуете этому совету, вы почти всегда не ошибетесь.

Существует, однако, один вид «результата», где лучше передать ссылку на результат с сайта вызова.

Это в том случае, если вы используете контейнер std в качестве результата буфера в цикле.

Если вы посмотрите на функцию std::getline, вы увидите пример.

std::getline предназначен для заполнения буфера std::string из входного потока.

Каждый раз, когда getline вызывается с той же ссылкой на строку, данные строки перезаписываются. Обратите внимание, что со временем (при условии случайных длин линий) иногда потребуется неявное reserve строки для размещения новых длинных строк. Однако более короткие строки, чем дольше всего, не потребуют reserve, так как уже будет достаточно capacity.

Представьте себе версию GetLine со следующей подписью:

std::string fictional_getline(std::istream&); 

Это означает, что новая строка возвращается каждый раз, когда функция вызывается. Независимо от того, было ли RVO или NRVO, эта строка должна быть создана, и если она длиннее границы оптимизации короткой строки, для этого потребуется выделение памяти. Кроме того, память строки будет освобождена каждый раз, когда она выходит за пределы области видимости.

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

примеры:

void do_processing(const std::string& s) 
{ 
    // ... 
} 

/// @post: in the case of an error, os.bad() == true 
/// @post: in the case of no error, os.bad() == false 
std::string fictional_getline(std::istream& stream) 
{ 
    std::string result; 
    if (not std::getline(stream, result)) 
    { 
     // what to do here? 
    } 
    return result; 
} 

// note that buf is re-used which will require fewer and fewer 
// reallocations the more the loop progresses 
void fast_process(std::istream& stream) 
{ 
    std::string buf; 
    while(std::getline(std::cin, buf)) 
    { 
     do_processing(buf); 
    } 
} 

// note that buf is re-created and destroyed each time around the loop  
void not_so_fast_process(std::istream& stream) 
{ 
    for(;;) 
    { 
     auto buf = fictional_getline(stream); 
     if (!stream) break; 
     do_processing(buf); 
    } 
} 
+0

Спасибо за ваш ответ. Если я создаю объект в цикле, я знаю, что он будет создавать и различать каждый раз, но что, если я улажу объект из цикла? В 'not_so_fast_process', я делаю это следующим образом:' std :: string buf; for (;;) {buf = fictional_getline (stream), если (! stream) break; do_processing (buf);} ', использует ли он' buf' как buf в 'fast_process'? – Regis

+0

@Reigs компилятору разрешено исключать конструктор копирования независимо от побочных эффектов, но не разрешается исключать оператора присваивания. В этом случае у вас все равно будет один избыточный цикл конструктора/деструктора для каждого цикла. –

+0

Я вижу. Если я удаляю buf из цикла, в этой строке 'buf = fictional_getline (stream);', будет скопировано все время из-за того, что это назначение, которое не может быть отменено. Таким образом, он будет менее эффективным. Спасибо. – Regis