Возврат против Не возврат функций?

Вернуть или не вернуть, это вопрос к функциям! Или это действительно имеет значение?


Здесь идет история:
Я использовал для написания кода, как показано ниже:

Type3 myFunc(Type1 input1, Type2 input2){}

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

void myFunc(Type1 input1, Type2 input2, Type3 &output){}

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


Для меня, я начинаю верить, что второй метод лучше в некоторых ситуациях, особенно у меня есть несколько вещей, чтобы вернуть или изменить. Например: вторая строка ниже будет лучше и быстрее первой, так как избегать копирования всего vecor<int> при возвращении.

vector<int> addTwoVectors(vector<int> a, vector<int> b){}
void addTwoVectors(vector<int> a, vector<int> b, vector<int> &result){}:

Но в некоторых других ситуациях я не могу купить его. Например,

bool checkInArray(int value, vector<int> arr){}

будет определенно лучше, чем

void checkInArray(int value, vector<int> arr, bool &inOrNot){}

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


Подводя итог, я озадачен (акцент на C ++):

  • Что должно быть возвращено функциями, а что нет (или стараться избегать)?
  • Есть ли какой-нибудь стандартный способ или хорошие предложения для меня, чтобы следовать?
  • Можем ли мы добиться большего успеха как в удобочитаемости, так и в эффективности кода?

редактировать:
Я знаю, что при некоторых условиях мы должны использовать один из них. Например, я должен использовать return-type functions если мне нужно добиться method chaining, Поэтому, пожалуйста, сосредоточьтесь на ситуациях, когда оба метода могут применяться для достижения цели.

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

4

Решение

Как всегда, когда кто-то выдвигает аргумент, что одна вещь быстрее другой, вы брали время? В полностью оптимизированном коде, на каждом языке и на каждом компиляторе, который вы планируете использовать? Без этого любой аргумент, основанный на производительности, является спорным.

Я вернусь к вопросу о производительности через секунду, просто позвольте мне сначала рассмотреть то, что я считаю более важным: конечно, есть веские причины для передачи параметров функции по ссылке. Основной момент, о котором я могу думать прямо сейчас, — это то, что параметр фактически является входным и выходным, то есть функция должна работать с существующими данными. Для меня это то, что указывает сигнатура функции с неконстантной ссылкой. Если такая функция затем игнорирует то, что уже находится в этом объекте (или, что еще хуже, явно ожидает получить когда-либо только созданную по умолчанию), этот интерфейс сбивает с толку.

Теперь вернемся к производительности. Я не могу говорить за C # или Java (хотя я полагаю, что возвращение объекта в Java не вызовет копирования в первую очередь, просто передача ссылки), а в C у вас нет ссылок, но может потребоваться прибегнуть к передаче указателей вокруг (и затем, я согласен, что передача указателя на неинициализированную память в порядке). Но в C ++ компиляторы долгое время занимались оптимизацией возвращаемого значения, RVO, что в основном означает, что в большинстве вызовов, таких как A a = f(b);, конструктор копирования обойден и f создаст объект прямо в нужном месте. В C ++ 11 мы даже получили семантику перемещения, чтобы сделать это явным и использовать его в других местах.

Должны ли вы просто вернуть A* вместо? Только если вам действительно не по душе старые времена ручного управления памятью. По крайней мере, вернуть std::shared_ptr<A> или std::unique_ptr<A>,

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

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

4

Другие решения

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

Давайте рассмотрим простой пример функции:

return_value foo( void );

Вот возможности, которые могут возникнуть:

  1. Оптимизация возвращаемого значения (RVO)
  2. Оптимизация именованного возвращаемого значения (NRVO)
  3. Переместить семантический возврат
  4. Копировать семантический возврат

Что такое Оптимизация возвращаемого значения? Рассмотрим эту функцию:

return_value foo( void ) { return return_value(); }

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

void call_foo( void )
{
return_value tmp = foo();
}

В этом примере tmp на самом деле напрямую используется в foo, как будто foo определил его, удалив все копии. Это ОГРОМНАЯ оптимизация, если return_value является нетривиальным типом.

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

Как насчет Оптимизация именованного возвращаемого значения?

Этот немного сложнее; если вы называете переменную перед тем, как ее вернуть, то теперь это l-значение. Это означает, что компилятор должен проделать больше работы, чтобы доказать, что построение на месте будет возможно:

return_type foo( void )
{
return_type bar;
// do stuff
return bar;
}

В общем, эта оптимизация все еще возможна, но менее вероятна для нескольких путей кода, если каждый путь кода не возвращает один и тот же объект; Возврат нескольких разных объектов из нескольких разных путей кода, как правило, не составляет труда оптимизировать:

return_type foo( void)
{
if(some_condition)
{
return_type bar = value;
return bar;
}
else
{
return_type bar2 = val2;
return bar2;
}
}

Это не будет так же хорошо получено. Все еще возможно, что NRVO может сработать, но это становится все менее и менее вероятным. Если это вообще возможно, создайте одно значение return_value и настройте его в разных путях кода, а не возвращайте полностью разные.

Если NRVO возможно, это избавит от любых накладных расходов; это будет так, как если бы оно было создано непосредственно в вызывающей функции.

Если ни одна из форм оптимизации возвращаемого значения невозможна, Переместить возврат может быть возможно

C ++ 11 и C ++ 03 оба имеют возможность делать семантику перемещения; Вместо того, чтобы копировать информацию из одного объекта в другой, семантика перемещения позволяет одному объекту украсть данные в другом, устанавливая его в состояние по умолчанию. Для семантики перемещения C ++ 03 вам нужно boost.move, но концепция по-прежнему здравая.

Возврат при перемещении не так быстр, как при возврате RVO, но он значительно быстрее, чем при копировании. Для совместимого компилятора C ++ 11, которого сегодня много, все структуры STL и STD должны поддерживать семантику перемещения. Ваши собственные объекты могут не иметь конструктора перемещения / оператора присваивания по умолчанию (MSVC в настоящее время не имеет семантических операций перемещения по умолчанию для пользовательских типов), но добавить семантику перемещения не сложно: просто используйте идиому копирования и замены, чтобы добавить его!

Что такое идиома копирования и обмена?

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

Однако в большинстве случаев это не будет значительно медленнее!

Для примитивных типов, таких как float или int или bool, копирование — это одно назначение или перемещение; вряд ли на что жаловаться; Передача таких вещей по ссылке без веской причины обязательно сделает ваш код медленнее, поскольку ссылки являются внутренними указателями. Для чего-то вроде вашего примера bool нет причин тратить время или энергию на передачу bool по ссылке; возвращение это самый быстрый способ.

Когда вы возвращаете что-то, что помещается в регистр, оно обычно возвращается в регистр именно по этой причине; это быстро, и, как уже было отмечено, проще всего поддерживать.

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

Если ваш тип является большим и внушительным типом, таким как std :: string или что-то с большим количеством данных за ним, требующим большого количества глубоких копий, и ваш код достаточно сложен, чтобы сделать RVO маловероятным, тогда, возможно, передача по ссылке лучшая идея.

Резюме

  1. Любые анонимные (rvalue) значения должны возвращаться по значению
  2. Малые или примитивные типы должны быть возвращены по значению.
  3. Любой тип, поддерживающий семантику перемещения (STL, STD и т. Д.), Должен возвращаться по значению
  4. Именованные (lvalue) значения, которые легко рассуждать, должны возвращаться по значению
  5. Большие типы данных в сложных функциях должны быть профилированы или переданы по ссылке

Всегда возвращайте значение по возможности, если вы используете C ++ 11. Это более разборчиво и быстрее.

4

На этот вопрос нет однозначного ответа, но, как вы уже сказали, центральная часть такова: это зависит.

Понятно, что для простых типов, таких как int или bools, возвращаемое значение обычно является предпочтительным решением. Его легче написать, а также он менее подвержен ошибкам (т. Е. Потому, что вы не можете передать что-то неопределенное в функцию и вам не нужно отдельно определять переменную перед инструкцией вызова). Для сложных типов, таких как коллекция, может быть предпочтителен вызов по ссылке, поскольку, как вы говорите, избегается дополнительный шаг копирования. Но вы также можете вернуть vector<int>* вместо просто vector<int>, который архивирует то же самое (за счет некоторого дополнительного управления памятью, хотя). Все это, однако, также зависит от используемого языка. Вышеуказанное в основном будет справедливо для C или C ++, но для управляемых классов, таких как Java или C #, большинство сложных типов в любом случае являются ссылочными типами, поэтому возвращение вектора не требует никакого копирования.

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

Итак, еще раз: это зависит.

3

Это различие между методами и функциями.

Методы (подпрограмма a.k.a.) вызываются, прежде всего, для побочного эффекта, который заключается в изменении одного или нескольких объектов, переданных в него в качестве параметра. В языках, которые поддерживают ООП, изменяемый объект обычно неявно передается как параметр / self.

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

Если функция / метод предназначены для создания нового объекта (то есть фабрики), то этот объект должен быть возвращен. Если вы передадите ссылку на переменную, тогда не ясно, кто будет отвечать за очистку объекта, ранее содержавшегося в переменной, вызывающий объект или фабрика? С завода функция, понятно, что вызывающая сторона отвечает за очистку предыдущего объекта; с завода метод, это не так ясно, потому что фабрика может проводить очистку, хотя это часто плохая идея по разным причинам.

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

Если ваши объекты неизменяемы, то вы всегда должны использовать функции, потому что каждая операция над неизменяемыми объектами должна создавать новый объект.

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

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

3

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

Когда ваш метод изменяет список или возвращает новые данные, вы должны использовать возвращаемое значение. Намного лучше понять, что делает ваш код, чем использовать параметр ref.

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

Вы можете написать такой код, который передает параметр списка от одного метода другому:

method1(list).method2(list)...
2

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

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

Таким образом, оставшийся вопрос — большие операнды.

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

Современные компиляторы — в первую очередь потому, что оперативная память намного больше в наши дни, а это делает жизнь намного лучше — не так уж глупы. Когда они видятreturn foo;«в функциональном теле и foo не вписывается в реестр, они помечают foo как ссылка на память. Это память, выделенная вызывающей стороной для хранения возвращаемого значения. Следовательно, компилятор генерирует почти именно так такой же код, как если бы вы передали ссылку для возврата значения самостоятельно.

Давайте проверим это. Вот простая программа.

struct Big {
int a[10000];
};

Big process(int n, int c)
{
Big big;
for (int i = 0; i < 10000; i++)
big.a[i] = n + i;
return big;
}

void process(int n, int c, Big& big)
{
for (int i = 0; i < 10000; i++)
big.a[i] = n + i;
}

Теперь я скомпилирую его с помощью компилятора XCode на моем MacBook. Вот соответствующий вывод для return версия:

    xorl    %eax, %eax
.align  4, 0x90
LBB0_1:                                 ## =>This Inner Loop Header: Depth=1
leal    (%rsi,%rax), %ecx
movl    %ecx, (%rdi,%rax,4)
incq    %rax
cmpl    $10000, %eax            ## imm = 0x2710
jne     LBB0_1
## BB#2:
movq    %rdi, %rax
popq    %rbp
ret

и для справочной версии:

    xorl    %eax, %eax
.align  4, 0x90
LBB1_1:                                 ## =>This Inner Loop Header: Depth=1
leal    (%rdi,%rax), %ecx
movl    %ecx, (%rdx,%rax,4)
incq    %rax
cmpl    $10000, %eax            ## imm = 0x2710
jne     LBB1_1
## BB#2:
popq    %rbp
ret

Даже если вы не читаете код на ассемблере, вы можете увидеть сходство. Возможно, есть одна разница в инструкции. Это с -O1, С отключенной оптимизацией код становится длиннее, но все же практически идентичен. С gcc версия 4.2, результаты очень похожи.

Так что вы должны сказать своим друзьям «нет». Использование возвращаемого значения с современным компилятором не имеет штрафов.

1

Для меня передача неконстантного указателя означает две вещи:

  • Параметр может быть изменен на месте (вы можете передать указатель на член структуры и избежать назначения);
  • Параметр не нужно возвращать, если null передается.

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

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

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

(Отказ от ответственности: я обычно не пишу на C.)

0