Как избежать параметров? - PullRequest
       12

Как избежать параметров?

8 голосов
/ 26 января 2010

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

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

Часть вторая моего вопроса касается некоторых конкретных случаев, с которыми я столкнулся, когда я хотел бы избежать параметра out, но не могу придумать, как это сделать.

Пример 1: У меня есть класс с дорогой копией, которую я хотел бы избежать. Работу можно выполнить над объектом, и это делает объект дорогим для копирования. Работа по сбору данных также не совсем тривиальна. В настоящее время я передам этот объект в функцию, которая будет изменять состояние объекта. Для меня это предпочтительнее, чем создавать новый объект внутри рабочей функции и возвращать его обратно, поскольку это позволяет мне хранить вещи в стеке.

class ExpensiveCopy //Defines some interface I can't change.
{
public:
    ExpensiveCopy(const ExpensiveCopy toCopy){ /*Ouch! This hurts.*/ };
    ExpensiveCopy& operator=(const ExpensiveCopy& toCopy){/*Ouch! This hurts.*/};

    void addToData(SomeData);
    SomeData getData();
}

class B
{
public:
    static void doWork(ExpensiveCopy& ec_out, int someParam);
    //or
    // Your Function Here.
}

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

const int SOME_PARAM = 5;
ExpensiveCopy toModify;
B::doWork(toModify, SOME_PARAM);

Я бы хотел что-то вроде этого:

ExpensiveCopy theResult = B::doWork(SOME_PARAM);

Но я не знаю, возможно ли это.

Второй пример: У меня есть массив объектов. Объекты в массиве являются сложным типом, и мне нужно выполнить работу над каждым элементом, работу, которую я хотел бы отделить от основного цикла, который обращается к каждому элементу. Код в настоящее время выглядит так:

std::vector<ComplexType> theCollection;
for(int index = 0; index < theCollection.size(); ++index)
{
    doWork(theCollection[index]);
}

void doWork(ComplexType& ct_out)
{
   //Do work on the individual element.
}

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

Ответы [ 9 ]

7 голосов
/ 26 января 2010

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

код

static void doWork(ExpensiveCopy& ec_out, int someParam);

выглядит прекрасно для меня.

Если вы действительно хотите изменить его, у вас есть несколько вариантов

  1. Переместите doWork, чтобы он был членом ExtifulCopy (что, как вы говорите, вы не можете сделать, значит, нет)
  2. возвращает (умный) указатель из doWork вместо его копирования. (что вы не хотите делать так, как хотите, чтобы все было в стеке)
  3. Положитесь на RVO (на что указывали другие, поддерживается почти всеми современными компиляторами)
3 голосов
/ 26 января 2010

Каждый полезный компилятор выполняет RVO (оптимизацию возвращаемого значения), если оптимизация включена, поэтому следующее фактически не приводит к копированию:

Expensive work() {
    // ... no branched returns here
    return Expensive(foo);
}

Expensive e = work();

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

Expensive work() {
    Expensive e; // named object
    // ... no branched returns here
    return e; // return named object
}

Это, однако, не совсем надежно, работает только в более тривиальных случаях и должно быть проверено. Если вам не нужно тестировать каждый случай, просто используйте out-параметры со ссылками во втором случае.

2 голосов
/ 26 января 2010

В дополнение ко всем комментариям здесь я бы упомянул, что в C ++ 0x вы редко используете выходной параметр для оптимизации - из-за конструкторов перемещения (см. здесь )

2 голосов
/ 26 января 2010

IMO, первое, что вы должны спросить себя, действительно ли копирование ExpensiveCopy настолько непомерно дорого.И чтобы ответить на это, вам обычно нужен профилировщик.Если профилировщик не скажет вам, что копирование действительно является узким местом, просто напишите код, который легче читать: ExpensiveCopy obj = doWork(param);.

Конечно, действительно есть случаи, когда объекты не могут быть скопированы из-за производительности или по другим причинам.Тогда ответ Нейла применяется.

1 голос
/ 26 января 2010

Если вы не пойдете по пути "все неизменно", который не слишком подходит для C ++.Вы не можете легко избежать параметров.Стандартная библиотека C ++ использует их, и то, что для этого достаточно, мне достаточно.

0 голосов
/ 26 января 2010

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

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

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

0 голосов
/ 26 января 2010

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

Простой факт заключается в том, что некоторые функции должны возвращать несколько вещей, и в большинстве языков это предлагает параметры. Common Lisp имеет multiple-value-bind и multiple-value-return, в которых список символов предоставляется связыванием и возвращается список значений. В некоторых случаях функция может возвращать составное значение, такое как список значений, которые затем будут деконструированы, и для функции C ++ не составит большого труда вернуть std::pair. Возвращать более двух значений таким способом в C ++ становится неловко. Всегда можно определить структуру, но ее определение и создание часто будет более сложным, чем параметры.

В некоторых случаях возвращаемое значение перегружается. В C getchar() возвращает int, поскольку идея состоит в том, что значений int больше, чем char (true во всех известных мне реализациях, false в некоторых, которые я легко могу себе представить), поэтому одно из значений можно использовать для обозначения конец-файл. atoi() возвращает целое число, либо целое число, представленное переданной строкой, либо ноль, если его нет, поэтому он возвращает то же самое для «0» и «лягушки». (Если вы хотите узнать, было ли значение int или нет, используйте strtol(), в котором есть параметр out.)

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

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

0 голосов
/ 26 января 2010

На какой платформе вы работаете?

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

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

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

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

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

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

0 голосов
/ 26 января 2010

Что касается вашего первого примера: оптимизация возвращаемого значения часто позволяет создавать возвращаемый объект непосредственно на месте, вместо того, чтобы копировать объект вокруг. Все современные компиляторы делают это.

...