Многократное назначение в одну строку

Я только сталкивался с утверждением во встроенном c (dsPIC33)

sample1 = sample2 = 0;

Это будет означать

sample1 = 0;

sample2 = 0;

Почему они так пишут? Это хорошее или плохое кодирование?

8 ответов

Решение

Помните, что присваивание выполняется справа налево и что это нормальные выражения. Так что с точки зрения компиляторов линия

sample1 = sample2 = 0;

такой же как

sample1 = (sample2 = 0);

который так же, как

sample2 = 0;
sample1 = sample2;

То есть, sample2 назначается ноль, то sample1 присваивается значение sample2, На практике то же самое, что присваивать оба нуля, как вы уже догадались.

Формально для двух переменных t а также u типа T а также U соответственно

T t;
U u;

назначение

t = u = X;

(где X какое-то значение) интерпретируется как

t = (u = X);

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

u = X;
t = (U) X;

Обратите внимание, что значение X должен достигать переменной t "как будто" он прошел через переменную u во-первых, но не требуется, чтобы это произошло буквально таким образом. X просто должен быть преобразован в тип u перед назначением t, Значение не обязательно должно быть присвоено u сначала, а затем скопированы из u в t, Два вышеупомянутых назначения на самом деле не упорядочены и могут происходить в любом порядке, что означает, что

t = (U) X;
u = X;

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

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

Я думаю, что нет хорошего ответа на языке C без фактического списка сборки:)

Итак, для упрощенной программы:

int main() {
        int a, b, c, d;
        a = b = c = d = 0;
        return a;
}

Я получил это в сборе (Kubuntu, gcc 4.8.2, x86_64) с -O0 вариант конечно;)

main:
        pushq   %rbp
        movq    %rsp, %rbp

        movl    $0, -16(%rbp)       ; d = 0
        movl    -16(%rbp), %eax     ; 

        movl    %eax, -12(%rbp)     ; c = d
        movl    -12(%rbp), %eax     ;

        movl    %eax, -8(%rbp)      ; b = c
        movl    -8(%rbp), %eax      ;

        movl    %eax, -4(%rbp)      ; a = b
        movl    -4(%rbp), %eax      ;

        popq    %rbp

        ret                         ; return %eax, ie. a

Так что GCC на самом деле цепочка всех вещей.

Как было замечено ранее,

sample1 = sample2 = 0;

равно

sample2 = 0;
sample1 = sample2;

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

sample2 = 0;
sample1 = 0;

Например, пусть UDR будет регистром данных UART. Чтение из UDR означает получение полученного значения из входного буфера, тогда как запись в UDR означает помещение желаемого значения в буфер передачи и установление связи. В таком случае,

sample = UDR = 0;

означает следующее: а) передать нулевое значение с помощью UART (UDR = 0;) и б) читать входной буфер и помещать данные в sample значение (sample = UDR;).

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

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

Вы можете сами решить, является ли этот способ кодирования хорошим или плохим.

  1. Просто посмотрите код сборки для следующих строк в вашей IDE.

  2. Затем измените код на два отдельных назначения и посмотрите на различия.

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

sample1 = sample2 = 0;

значит

sample1 = 0;
sample2 = 0;  

если и только если sample2 объявлено ранее.
Вы не можете сделать это так:

int sample1 = sample2 = 0; //sample1 must be declared before assigning 0 to it

Как уже говорили другие, порядок их выполнения является детерминированным. Приоритет оператора оператора = гарантирует, что это выполняется справа налево. Другими словами, это гарантирует, что sample2 передается значение перед sample1.

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

sample1 = func() + (sample2 = func());

тогда приоритет оператора гарантирует тот же порядок выполнения, что и раньше (+ имеет более высокий приоритет, чем =, поэтому в скобках). sample2 будет присвоено значение перед sample1. Но в отличие от приоритета операторов, порядок вычисления операторов не является детерминированным, это неопределенное поведение. Мы не можем знать, что самый правый вызов функции вычисляется раньше, чем самый левый.

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

int tmp1 = func();
int tmp2 = func();
sample2 = tmp2;
sample1 = tmp1 + tmp2;

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


(*) MISRA-C: 2004 12,2, MISRA-C:2012 13,4, CERT-C EXP10-C.

Позаботьтесь об этом особом случае... предположим, что b является массивом структуры вида

{
    int foo;
}

и позвольте мне быть смещением в б. Рассмотрим функцию realloc_b (), возвращающую int и выполняющую перераспределение массива b. Рассмотрим это множественное назначение:

a = (b + i)->foo = realloc_b ();

Чтобы мой опыт (b + i) был решен первым, скажем, это b_i в ОЗУ; затем выполняется realloc_b (). Поскольку realloc_b () изменяет b в ОЗУ, это приводит к тому, что b_i больше не выделяется.

Переменная a назначена правильно, но (b + i)->foo не потому, что b было изменено при выполнении самого левого члена назначения, т.е. realloc_b ()

Это может вызвать ошибку сегментации, поскольку b_i потенциально находится в нераспределенной области ОЗУ.

Чтобы быть безошибочным и иметь (b + i)->foo равным a, однострочное назначение должно быть разбито на два назначения:

a = reallocB();
(b + i)->foo = a;

Относительно стиля кодирования и различных рекомендаций по кодированию смотрите здесь: Удобочитаемость a=b=c или a = c; B = C;?

Я считаю, что с помощью

sample1 = sample2 = 0;

некоторые компиляторы производят сборку немного быстрее по сравнению с 2 назначениями:

sample1 = 0;
sample2 = 0;

особенно если вы инициализируете ненулевое значение. Потому что множественное назначение переводится как:

sample2 = 0; 
sample1 = sample2;

Таким образом, вместо 2 инициализаций вы делаете только одну и одну копию. Ускорение (если есть) будет крошечным, но во встроенном случае каждый крошечный бит имеет значение!

Другие вопросы по тегам