оператор if — оценка короткого замыкания против удобочитаемости

#c #if-statement #short-circuiting #side-effects

#c #оператор if #короткое замыкание #побочные эффекты

Вопрос:

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

например, это:

 if (SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall())
{
    // do stuff
}
 

в это

 bool b1 = SomeComplicatedFunctionCall();
bool b2 = OtherComplicatedFunctionCall();

if (b1 || b2)
{
    //do stuff
}
 

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

Но с этим извлечением я потерял оценку короткого замыкания (SCE).

  1. Я действительно теряю SCE каждый раз? Существует ли какой-либо сценарий, в котором компилятору разрешено «оптимизировать его» и при этом предоставлять SCE?
  2. Существуют ли способы сохранить улучшенную удобочитаемость второго фрагмента без потери SCE?

Комментарии:

1. Практика показывает, что большинство ответов о производительности, которые вы увидите здесь или в других местах, в большинстве случаев неверны (4 неправильных 1 правильный). Мой совет: всегда выполняйте профилирование и проверяйте его самостоятельно, вы избежите «преждевременной оптимизации» и узнаете что-то новое.

2. @MarekR речь идет не только о производительности, но и о возможных побочных эффектах при вызове других функций…

3. @David при обращении к другим сайтам часто бывает полезно указать, что перекрестная публикация не одобряется

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

5. Потенциальные близкие избиратели: прочитайте вопрос еще раз. Часть (1) не основана на мнениях, в то время как часть (2) может легко перестать основываться на мнениях посредством редактирования, которое удаляет ссылку на любую предполагаемую «наилучшую практику», как я собираюсь сделать.

Ответ №1:

Одно естественное решение будет выглядеть так:

 bool b1 = SomeCondition();
bool b2 = b1 || SomeOtherCondition();
bool b3 = b2 || SomeThirdCondition();
// any other condition
bool bn = bn_1 || SomeFinalCondition();

if (bn)
{
  // do stuff
}
 

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


Это было мое первоначальное решение: хорошим шаблоном в вызовах методов и телах цикла for является следующее:

 if (!SomeComplicatedFunctionCall())
   return; // or continue

if (!SomeOtherComplicatedFunctionCall())
   return; // or continue

// do stuff
 

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

Комментарии:

1. @relaxxx: Я понимаю тебя, но «еще кое-что нужно сделать после if » также является признаком того, что ваша функция или метод слишком велики и должны быть разделены на более мелкие. Это не всегда лучший способ, но очень часто так и есть!

2. это нарушает принцип белого списка

3. @JoulinRouge: Интересно, я никогда не слышал об этом принципе. Я сам предпочитаю этот подход «короткого замыкания» из-за преимуществ для удобства чтения: он уменьшает отступы и исключает вероятность того, что что-то произойдет после блока с отступом.

4. Это более читабельно? Назовите b2 правильно, и вы получите someConditionAndSomeotherConditionIsTrue , не очень значимый. Кроме того, я должен хранить кучу переменных в своем ментальном стеке во время этого упражнения (и tbh, пока я не перестану работать в этой области). Я бы выбрал SJuan76 решение № 2 или просто поместил все это в функцию.

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

Ответ №2:

Я склонен разбивать условия на несколько строк, т.е.:

 if( SomeComplicatedFunctionCall()
 || OtherComplicatedFunctionCall()
  ) {
 

Даже при работе с несколькими операторами (amp;amp; ) вам просто нужно увеличить отступ с каждой парой скобок. SCE все еще работает — нет необходимости использовать переменные. Написание кода таким образом сделало его гораздо более понятным для меня уже много лет. Более сложный пример:

 if( one()
 ||( two()> 1337
  amp;amp;( three()== 'foo'
   || four()
    )
   )
 || five()!= 3.1415
  ) {
 

Ответ №3:

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

 bool b = SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall();
if (b amp;amp; some_other_expression) { ... }
 

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

 auto e = []()
{
    return SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall();
};

if (e() amp;amp; some_other_expression) { ... }
 

Ответ №4:

1) Да, у вас больше нет SCE. В противном случае у вас было бы это

 bool b1 = SomeComplicatedFunctionCall();
bool b2 = OtherComplicatedFunctionCall();
 

работает так или иначе, в зависимости от того, есть ли if оператор позже. Слишком сложный.

2) Это основано на мнениях, но для достаточно сложных выражений вы можете сделать:

 if (SomeComplicatedFunctionCall()
    || OtherComplicatedFunctionCall()) {
 

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

Ответ №5:

Вы также можете использовать:

 bool b = someComplicatedStuff();
b = b || otherComplicatedStuff(); // it has to be: b = b || ...;  b |= ...; is bitwise OR and SCE is not working then 
 

и SCE будет работать.

Но это не намного более читабельно, чем, например:

 if (
    someComplicatedStuff()
    ||
    otherComplicatedStuff()
   )
 

Комментарии:

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

2. Я специально использовал b = b || otherComplicatedStuff(); и @SargeBorsch внес правку, чтобы удалить SCE. Спасибо, что заметили меня об этом изменении @Ant.

Ответ №6:

1) Действительно ли я каждый раз теряю SCE? Является ли компилятор каким-либо сценарием, позволяющим «оптимизировать его» и по-прежнему обеспечивать SCE?

Я не думаю, что такая оптимизация разрешена; особенно OtherComplicatedFunctionCall() могут быть некоторые побочные эффекты.

2) Какова наилучшая практика в такой ситуации? Это единственная возможность (когда я хочу SCE) иметь все, что мне нужно, непосредственно внутри if и «просто отформатировать его так, чтобы оно было максимально читаемым»?

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

 bool getSomeResult() {
    return SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall();
}

...

if (getSomeResult())
{
    //do stuff
}
 

И поскольку мы реализуем getSomeResult() на основе SomeComplicatedFunctionCall() и OtherComplicatedFunctionCall() , мы могли бы рекурсивно разложить их, если они все еще сложны.

Комментарии:

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

Ответ №7:

1) Действительно ли я каждый раз теряю SCE? Является ли компилятор каким-либо сценарием, позволяющим «оптимизировать его» и по-прежнему обеспечивать SCE?

Нет, вы этого не делаете, но он применяется по-другому:

 if (SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall())
{
    // do stuff
}
 

Здесь компилятор даже не будет запускаться OtherComplicatedFunctionCall() , если SomeComplicatedFunctionCall() возвращает true .

 bool b1 = SomeComplicatedFunctionCall();
bool b2 = OtherComplicatedFunctionCall();

if (b1 || b2)
{
    //do stuff
}
 

Здесь обе функции будут выполняться, потому что они должны быть сохранены в b1 и b2 . Ff b1 == true тогда b2 не будет оцениваться (SCE). Но OtherComplicatedFunctionCall() уже был запущен.

If b2 больше нигде не используется, компилятор может быть достаточно умен, чтобы встроить вызов функции внутри if, если функция не имеет заметных побочных эффектов.

2) Какова наилучшая практика в такой ситуации? Это единственная возможность (когда я хочу SCE) иметь все, что мне нужно, непосредственно внутри if и «просто отформатировать его так, чтобы оно было максимально читаемым»?

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

Ответ №8:

Еще одна возможность короткого замыкания и наличия условий в одном месте:

 bool (* conditions [])()= {amp;a, amp;b, ...}; // list of conditions
bool conditionsHold = true;
for(int i= 0; i < sizeOf(conditions); i   ){
     if (!conditions[i]()){;
         conditionsHold = false;
         break;
     }
}
//conditionsHold is true if all conditions were met, otherwise false
 

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

Комментарии:

1. @Erbureth Нет, это не так. Элементы массива являются указателями на функции, они не выполняются до тех пор, пока функции не будут вызваны в цикле.

2. Спасибо, Бармар, но я внес правку, Эрбурет был прав, перед редактированием (я думал, что моя правка будет продвигать визуально более непосредственно).

Ответ №9:

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

 if (somecomplicated_function() || // let me explain what this function does
    someother_function())         // this function does something else
...
 

Кроме того, я всегда предваряю свои функции некоторыми комментариями о самой функции, о ее вводе и выводе, и иногда я привожу пример, как вы можете видеть здесь:

 /*---------------------------*/
/*! interpolates between values
* @param[in] X_axis : contains X-values
* @param[in] Y_axis : contains Y-values
* @param[in] value  : X-value, input to the interpolation process
* @return[out]      : the interpolated value
* @example          : interpolate([2,0],[3,2],2.4) -> 0.8
*/
int interpolate(std::vector<int>amp; X_axis, std::vector<int>amp; Y_axis, int value)
 

Очевидно, что форматирование, используемое для ваших комментариев, может зависеть от вашей среды разработки (Visual studio, JavaDoc в Eclipse, …)

Что касается SCE, я предполагаю, что под этим вы подразумеваете следующее:

 bool b1;
b1 = somecomplicated_function(); // let me explain what this function does
bool b2 = false;
if (!b1) {                       // SCE : if first function call is already true,
                                 // no need to spend resources executing second function.
  b2 = someother_function();     // this function does something else
}

if (b1 || b2) {
...
}
 

Ответ №10:

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

Комментарии:

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