Использование оператора C запятой

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

20 ответов

Решение

Язык C (как и C++) исторически представляет собой смесь двух совершенно разных стилей программирования, которые можно назвать "программирование операторов" и "программирование выражений". Как вы знаете, каждый процедурный язык программирования обычно поддерживает такие фундаментальные конструкции, как секвенирование и ветвление (см. Структурное программирование). Эти фундаментальные конструкции присутствуют в языках Си / Си ++ в двух формах: одна для программирования операторов, другая для программирования выражений.

Например, когда вы пишете свою программу в терминах операторов, вы можете использовать последовательность операторов, разделенных ;, Когда вы хотите сделать ветвление, вы используете if заявления. Вы также можете использовать циклы и другие виды операторов передачи управления.

В программировании выражений вам доступны те же конструкции. Это на самом деле, где , оператор вступает в игру. оператор , не чем иным, как разделителем последовательных выражений в C, т. е. оператором , в программировании выражений выполняет ту же роль, что и ; делает в программировании оператора. Ветвление в программировании выражений осуществляется через ?: оператор и, альтернативно, через свойства оценки короткого замыкания && а также || операторы. (В программировании выражений нет циклов. И чтобы заменить их рекурсией, вам придется применить программирование операторов.)

Например, следующий код

a = rand();
++a;
b = rand();
c = a + b / 2;
if (a < c - 5)
  d = a;
else
  d = b;

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

a = rand(), ++a, b = rand(), c = a + b / 2, a < c - 5 ? d = a : d = b;

или как

a = rand(), ++a, b = rand(), c = a + b / 2, d = a < c - 5 ? a : b;

или же

d = (a = rand(), ++a, b = rand(), c = a + b / 2, a < c - 5 ? a : b);

или же

a = rand(), ++a, b = rand(), c = a + b / 2, (a < c - 5 && (d = a, 1)) || (d = b);

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

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

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

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

Вы можете использовать его, например, для упаковки нескольких операторов в тернарный оператор (?:), ala:

int x = some_bool ? printf("WTF"), 5 : fprintf(stderr, "No, really, WTF"), 117;

но боги мои, почему?!? (Я видел, как он использовался таким образом в реальном коде, но, к сожалению, у меня нет доступа к нему)

Особенности двух запятых оператора C++:

а) Чтение из потока до тех пор, пока не встретится конкретная строка (помогает сохранить код СУХИМЫМ):

 while (cin >> str, str != "STOP") {
   //process str
 }

б) Написать сложный код в инициализаторах конструктора:

class X : public A {
  X() : A( (global_function(), global_result) ) {};
};

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

Упрощенный пример:

#define SomeMacro(A) ( DoWork(A), Permute(A) )

Вот B=SomeMacro(A) "возвращает" результат Permute(A) и присваивает его "B".

Библиотека Boost Assignment является хорошим примером перегрузки оператора запятой полезным и удобочитаемым способом. Например:

using namespace boost::assign;

vector<int> v; 
v += 1,2,3,4,5,6,7,8,9;

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

Я не мог не записать сообщение в тело производного конструктора блокировки, поэтому мне пришлось поместить его в аргументы конструктора базового класса, используя: baseclass( ( log( "message"), actual_arg)) в списке инициализации. Обратите внимание на дополнительные скобки.

Вот выдержка из классов:

class NamedMutex : public boost::timed_mutex
{
public:
    ...

private:
    std::string name_ ;
};

void log( NamedMutex & ref__ , std::string const& name__ )
{
    LOG( name__ << " waits for " << ref__.name_ );
}

class NamedUniqueLock : public boost::unique_lock< NamedMutex >
{
public:

    NamedUniqueLock::NamedUniqueLock(
        NamedMutex & ref__ ,
        std::string const& name__ ,
        size_t const& nbmilliseconds )
    :
        boost::unique_lock< NamedMutex >( ( log( ref__ , name__ ) , ref__ ) ,
            boost::get_system_time() + boost::posix_time::milliseconds( nbmilliseconds ) ),
            ref_( ref__ ),
            name_( name__ )
    {
    }

  ....

};

Из стандарта С:

Левый операнд оператора запятой оценивается как пустое выражение; после его оценки есть точка последовательности. Затем вычисляется правый операнд; результат имеет свой тип и значение. (Оператор запятой не дает lvalue.)) Если предпринята попытка изменить результат оператора запятой или получить к нему доступ после следующей точки последовательности, поведение не определено.

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

Обратите внимание, что:

int a, b, c;

это НЕ оператор запятой, это список деклараторов.

Иногда используется в макросах, таких как макросы отладки, например:

#define malloc(size) (printf("malloc(%d)\n", (int)(size)), malloc((size)))

(Но посмотрите на этот ужасный провал, по-настоящему, на то, что может случиться, если вы переборщите.)

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

Вы можете перегрузить его (если в этом вопросе есть тег "C++"). Я видел некоторый код, где перегруженная запятая использовалась для генерации матриц. Или векторы, я точно не помню. Разве это не красиво (хотя немного запутанно):

MyVector foo = 2, 3, 4, 5, 6;

Вне цикла for, и даже если он есть, может иметь запах запаха кода, единственное место, которое я видел в качестве полезного оператора запятой, это как часть удаления:

 delete p, p = 0;

Единственное значение по сравнению с альтернативой - вы можете случайно скопировать / вставить только половину этой операции, если она выполняется в две строки.

Мне также нравится это, потому что, если вы делаете это по привычке, вы никогда не забудете нулевое назначение. (Конечно, почему p не находится внутри некоторой оболочки auto_ptr, smart_ptr, shared_ptr и т. Д., Это другой вопрос.)

Это очень полезно для добавления некоторых комментариев в ASSERT макросы:

ASSERT(("This value must be true.", x));

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

Учитывая цитату @Nicolas Goy из стандарта, звучит так, что вы могли бы написать однострочную для таких циклов:

int a, b, c;
for(a = 0, b = 10; c += 2*a+b, a <= b; a++, b--);
printf("%d", c);

Но, боже мой, мужик, ты правда хочешь сделать свой C-код более неясным?

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

foo=bar*2, plugh=hoo+7;

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

foo=bar*2;
plugh=hoo+7;

В одном месте, кроме циклов, где я использовал его, он создает if / else, например:

if (a==1)
... do something ...
else if (function_with_side_effects_including_setting_b(), b==2)
... do something that relies on the side effects ...

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

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

void* s_static_pointer = 0;

void init() {
    configureLib(); 
    s_static_pointer = calculateFancyStuff(x,y,z);
    regptr(s_static_pointer);
}

bool s_init = init(), true; // just run init() before anything else

Foo::Foo() {
  s_static_pointer->doStuff(); // works properly
}

Для меня один действительно полезный случай с запятыми в C - это использовать их для выполнения чего-либо условно.

  if (something) dothis(), dothat(), x++;

это эквивалентно

  if (something) { dothis(); dothat(); x++; }

Дело не в том, чтобы "меньше печатать", просто иногда выглядит очень четко.

Также циклы просто так:

while(true) x++, y += 5;

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

Это может быть удобно для "кода гольфа":

Код Гольф: игра в кубики

, в if(i>0)t=i,i=0; сохраняет два символа.

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

#define ASSIGN_INCR(p, val, type)  ((*((type) *)(p) = (val)), (p) += sizeof(type))

Использование оператора запятой означает, что макрос можно использовать в выражениях или в виде операторов по желанию:

if (need_to_output_short)
    ASSIGN_INCR(ptr, short_value, short);

latest_pos = ASSIGN_INCR(ptr, int_value, int);

send_buff(outbuff, (int)(ASSIGN_INCR(ptr, last_value, int) - outbuff));

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

Пожалуйста, посмотрите мою слишком длинную версию этого ответа здесь.

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

int ans = isRunning() ? total += 10, newAnswer(total) : 0;

Очевидно, что ни один здравомыслящий человек не написал бы такой код, но автор был злым гением, который строил c-операторы на основе сгенерированного ими кода ассемблера, а не читабельности. Например, он иногда использовал циклы вместо операторов if, потому что предпочитал сгенерированный им ассемблер.

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

У qemu есть некоторый код, который использует оператор запятой в условной части цикла for (см. QTAILQ_FOREACH_SAFE в qemu-queue.h). То, что они сделали, сводится к следующему:

#include <stdio.h>

int main( int argc, char* argv[] ){
  int x = 0, y = 0;

  for( x = 0; x < 3 && (y = x+1,1); x = y ){
    printf( "%d, %d\n", x, y );
  }

  printf( "\n%d, %d\n\n", x, y );

  for( x = 0, y = x+1; x < 3; x = y, y = x+1 ){
    printf( "%d, %d\n", x, y );
  }

  printf( "\n%d, %d\n", x, y );
  return 0;
}

... со следующим выводом:

0, 1
1, 2
2, 3

3, 3

0, 1
1, 2
2, 3

3, 4

Первая версия этого цикла имеет следующие эффекты:

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

Нашел это в инициализации массива:

В C, что именно происходит, если я использую () для инициализации массива двойного измерения вместо {}?

Когда я инициализирую массив a[][]:

int a[2][5]={(8,9,7,67,11),(7,8,9,199,89)};

а затем отобразить элементы массива.

Я получил:

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