Каковы преимущества сплющивания присвоения и проверки ошибок в одной строке?

Этот вопрос вдохновлен этим вопросом, который показывает следующий фрагмент кода.

int s;
if((s = foo()) == ERROR)
    print_error();

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

int s = foo();
if(s == ERROR)
    print_error();

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

19
задан Community 23 May 2017 в 11:47
поделиться

7 ответов

Когда вы пишете цикл, иногда желательно использовать первую форму, как в этом известном примере от K&R:

int c;

while ((c = getchar()) != EOF) {
    /* stuff */
}

Не существует элегантного «второго варианта» способа написать это без повторения:

int c = getchar();

while (c != EOF) {
    /* stuff */
    c = getchar();
}

Или:

int c;

for (c = getchar(); c != EOF; c = getchar()) {
    /* stuff */
}

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

Итак, нужно уметь легко научиться читать и писать первую форму. Учитывая это, кажется логичным использовать ту же форму в условиях if .

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

13
ответ дан 30 November 2019 в 03:24
поделиться

Я всегда выбираю второй. Его легче читать, нет опасности опустить круглые скобки вокруг присваивания, и его легче выполнить с помощью отладчика.

4
ответ дан 30 November 2019 в 03:24
поделиться

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

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

15
ответ дан 30 November 2019 в 03:24
поделиться

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

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

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

1
ответ дан 30 November 2019 в 03:24
поделиться

Я часто предпочитаю первую форму. Я не могу точно сказать почему, но это как-то связано с семантикой.

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

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

0
ответ дан 30 November 2019 в 03:24
поделиться

Я делаю сознательную попытку комбинировать и то, и другое, когда это возможно. ИМО, «штрафа» в размере недостаточно, чтобы преодолеть преимущество в ясности.

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

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

5
ответ дан 30 November 2019 в 03:24
поделиться

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

Это также позволяет мне более легко делать такие вещи, как:

int rc = function();

DEBUG_PRINT(rc);

if (rc == ERROR) {
    recover_from_error();
} else {
    keep_on_going(rc);
}

Я настолько предпочитаю этот стиль, что в случае циклов я бы предпочел:

while (1) {
    int rc = function();
    if (rc == ERROR) {
        break;
    }
    keep_on_going(rc);
}

, чем выполнять присваивание в , в то время как условное . Мне очень не нравится, что у моих тестов есть побочные эффекты.

1
ответ дан 30 November 2019 в 03:24
поделиться
Другие вопросы по тегам:

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