Использование оператора 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, а не в коде выше).
- Внутри цикла у вас есть доступ к текущему и следующему элементу
Нашел это в инициализации массива:
Когда я инициализирую массив 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