Рубрика «Без рубрики»

Случайно придумал quine на C

Для тех, кто не знает: quine (квайн) — это программа, которая при запуске выводит свой исходный текст.

Что интересно, мой вариант более чем вдвое короче, чем представленный в Википедии.

main(){char*p="main(){char*p=%c%s%c,c='%c',s[256];sprintf(s,p,c,p,c,c);puts(s);}",c='"',s[256];sprintf(s,p,c,p,c,c);puts(s);}

Изобрел я его совершенно случайно как побочный эффект своих рабочих дел. Опечатавшись, указал в printf() форматную строку как один из подставляемых аргументов, подставив тем самым форматную строку саму в себя. Сразу появились ассоциации с квайнами, повозился минут 15 — и готово. Заодно запостил в соответствующий раздел Codegolf@Stackexchange.

Добавлено

Оказывается, этот подход уже сто лет назад как придумали, и программа намного короче:

char*f="char*f=%c%s%c;main(){printf(f,34,f,34,10);}%c";main(){printf(f,34,f,34,10);}

В общем, я изобрел велосипед, ура.

УжасноПлохоНормальноХорошоОтлично (1 голосов, средний: 3,00 из 5)
Loading ... Loading ...

Как отбросить непрочитанные символы из cin?

Зачем отбрасывать символы? Попробуем решить такую задачу: нужно прочитать из cin целое число, причем если вместо целого числа нам подсунут какую-нибудь гадость, нужно сообщить об этом и попытаться прочитать число снова. Попытка номер один:

1
2
3
4
5
6
7
using namespace std;
int value = 0;
while (not (cin >> value)) {
    cout << "Invalid format" << endl;
    cin.clear();
}
cout << "Your input: " << value << endl;

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

Беглый гуглеж приводит меня к функции istream::sync(), которая, как говорит один источник, делает именно то, что нам нужно:

Synchronizes the buffer associated with the stream to its controlled input sequence. This effectively means that the unread characters in the buffer are discarded.

Что ж, попробуем:

1
2
3
4
5
6
7
8
using namespace std;
int value = 0;
while (not (cin >> value)) {
    cout << "Invalid format" << endl;
    cin.clear();
    cin.sync();
}
cout << "Your input: " << value << endl;

Хм. В моей системе результат не изменился, код работает точно так же, как и первый. Беда в том, что функция istream::sync() в некоторых реализациях стандартной библиотеки таки отбрасывает непрочитанные символы. Но не в моем случае.

А я человек простой — лезу в код своей стандартной библиотеки и смотрю что да как. Выходит, что istream::sync() вызывает stdio_sync_filebuf::pubsync(), которая вызывает stdio_sync_filebuf::sync(), которая, наконец, вызывает fflush(stdin). Понятно, что fflush(stdin) не может оказать никакого влияния на входной буфер cin.

Так что упомянутый выше «один источник» не стоит считать истиной в последней инстанции. Но надо сказать, что попытки найти истину в стандарте — еще более неблагодарное занятие.

В общем, правильное решение заключается в использовании функции istream::ignore() таким вот образом:

1
2
3
4
5
6
7
8
using namespace std;
int value = 0;
while (not (cin >> value)) {
    cout << "Invalid format" << endl;
    cin.clear();
    cin.ignore(numeric_limits<streamsize>::max(), '\n');
}
cout << "Your input: " << value << endl;

Надо отметить, что вызов ignore() неплохо бы вставлять даже после успешного ввода, так как при вводе строки «123qwe» первые три символа будут прочитаны и распознаны как число, а «qwe» останутся во входном буфере и будут портить жизнь при следующем чтении. Еще одна тонкость — обработка конца файла. Если пользователь в ответ на приглашение к вводу нажмет Ctrl+D или что там в вашей системе для этого предназначено, то программа вместо того, чтобы завершиться, сообщит о неверном формате и предложит ввести число еще раз. Поэтому по-хорошему код должен быть вроде этого (вся программа):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <limits>
using namespace std;
template <typename Type>
bool read_value(const std::string &prompt, Type &value)
{
    bool repeat = true;
    bool eof = cin.eof();
    while (repeat and not eof) {
        cout << prompt << ": ";
        cin >> value;
        eof = cin.eof();
        repeat = cin.fail() and not eof;
        if (repeat) {
            cout << "***Invalid data format!" << endl;
            cin.clear();
        } else if (eof) {
            cout << "(EOF)" << endl;
        }
        cin.ignore(numeric_limits<streamsize>::max(), '\n');
    }
    return not eof;
}
int main()
{
    int value = 0;
    while (read_value("Input integer", value))
        cout << "Your input: " << value << endl;
}

Но и в этом, казалось бы, безошибочном, коде затаилась пара граблей. Например, что если функцию read_value() использовать для ввода символов или строк? Сумеем мы ввести символ пробела? Сумеем ли ввести строку из нескольких слов, разделенных пробелами? Хрен! С вводом символа пробела еще несложно — надо только использовать манипулятор noskipws, а вот ввод строк с пробелами потребует перегрузки read_value() и использования getline() вместо оператора >>.

В общем, писать на C++ — сплошное счастье.
УжасноПлохоНормальноХорошоОтлично (1 голосов, средний: 5,00 из 5)
Loading ... Loading ...

Bash как mainstream-язык

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

Теория крючков

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

  1. На младших курсах все студенты изучают математический анализ. Это очень непростой для неокрепших умов предмет, и самое непростое в нем то, что когда его изучаешь, ты абсолютно не понимаешь, зачем все это нужно. Понять доказательство теоремы Вейерштрасса или разложение в ряд Тейлора в принципе можно, но должно пройти немало времени, прежде чем может быть у кого-то возникнет дежа вю — «Кажется, что-то такое я учил на первом курсе… Но уже нифига не помню». Хуже всего, если эти знания действительно нужны, и приходится открывать учебники и учиться заново. Во второй раз изучать получается быстрее, но все равно время жалко.
  2. На младших курсах многие студенты изучают еще и разнообразную физику. Это вообще труба, потому что используемый в лекциях матаппарат студенты еще не проходили. В результате физику никто не знает, а когда приходит время изучать тот матаппарат, то параллели провести тоже не получается — едва ли кто-то воскликнет «Так вот что, оказывается, имел в виду наш физик год назад, когда говорил о линейных операторах!».

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

Мне придумалась образное объяснение, почему так происходит — теория крючков. Она состоит из следующих постулатов:

  1. Поглощаемые человеком знания ищут в мозгу крючки, за которые могут зацепиться. Чем больше таких крючков, за которые зацепилось некоторое знание, тем дольше оно останется в мозгу.
  2. Крючки типизированные: определенным знаниям нужны определенные крючки. Нельзя зацепить знания о языке Фортран за крючки, относящиеся к плаванию кролем.
  3. Хотя некоторые уникумы умудряются нарушать типизацию крючков, проводя смелые аналогии в различных областях знаний.
  4. Закрепленные в мозгу новые знания сами начинают создавать крючки. Больше знаний — больше крючков — проще усваиваются новые знания.

Попробую продемонстрировать эту теорию на ней же самой. Эти постулаты сами по себе не очень ценны — какие-то утверждения, сделанные неизвестно кем непонятно с какой целью. Если трактовать их именно так, то на следующий день после прочтения они из головы сотрутся. Но можно прицепить их крючками к уже известным фактам, наведя между ними «знания-мосты». Такими мостами могут быть, например, следующие факты:

  • Когда знаешь много языков программирования (или иностранных языков), изучить еще один — гораздо более простая задача, чем изучить первый. Новый язык цепляется за множество крючков, выращенных уже изученными языками.
  • Система крючков гуманитария кардинально отличается от оной у технаря. Поэтому технические понятия гуманитарии воспринимают с трудом — крючки не подходят!
  • Проводить аналогии между программированием и постройкой домов, литературой, живописью, вышиванием крестиком или выращиванием цветов — нарушение типизации крючков. Но кто сказал, что это плохо?
  • Теория, пересыпанная примерами, изучается куда проще, чем сначала куча теории, а потом куча примеров. Поглощая знания маленькими кусочками, мы цепляем их друг к другу, не давая им успеть забыться. С большими кусками сложнее. Это все равно как есть очень длинную колбасу — пока доедаешь ее конец, начало уже давно переварилось и путешествует по канализации.
  • В этом посте неимоверное, зашкаливающее количество употреблений слова «крючок».

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

УжасноПлохоНормальноХорошоОтлично (4 голосов, средний: 4,75 из 5)
Loading ... Loading ...

Определить, поддерживает ли компилятор C вложенные комментарии

Такая вот классическая задача, которая которую я когда-то увидел в книжке «C Traps and Pitfalls». Для современных компиляторов ответ однозначен: вложенные комментарии вида /*…/*…*/…*/ не поддерживаются, см. п. 6.4.9-1 стандарта C99. Но во времена динозавров, говорят, некоторые компиляторы все-таки поддерживали. Собственно, задача формулируется так:

Написать программу на C, компилирующуюся без ошибок (warning-и не в счет), которая при запуске выводит «YES», если вложенные комментарии поддерживаются, и «NO», если это не так.

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

/* всегда в комментарии /* всегда в комментарии */ какой-то-код */

Если вложенность поддерживается, какой-то-код — всего лишь часть комментария. Но если вложенности нет, то комментарий кончается на первом же «*/», а какой-то-код будет выполнен. Проблема только в одном: что делать с остающимися в хвосте «*/»? Наверное, нужно где-то раньше поместить парные «/*», но тогда код станет некорректным в случае, если вложенность поддерживается. Нетрудно видеть, что написание комментариев с еще большей глубиной вложенности лишь усугубляет проблему.

(Здесь самое время прекратить читать и попытаться придумать решение самостоятельно.)

Ключ к решению — различная лексическая трактовка символов в зависимости от контекста. Я придумал три способа:

  1. Использовать двойные кавычки. Символы между парными кавычками становятся частью строкового литерала. Конечно, придется использовать два набора вложенных комментариев.
  2. Использовать символ «*», в зависимости от ситуации, как оператор разыменования или оператор умножения. Опять нужно два набора комментариев.
  3. Поглотить «*/», сделав эту строку содержимым символа препроцессора. Здесь нужно также вспомнить, что комментарии обрабатываются препроцессором до обработки макродиректив.

Код выглядит немного странно, но разобраться несложно:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// file: nestcomm.c?
#include <stdio.h>
void method1(){
    printf("Method 1: ");
    printf("%s\n", /*/**/"  NO\0*/"/*YES\0"/**/"*/"+2);
}
void method2(){
    int a = 1;
    printf("Method 2: ");
    printf("%s\n", /*/**/2*/*(&a)+/**/2==4?"NO":"YES");
}
void method3(){
    printf("Method 3: ");
    /*/**/#define A */
    printf("%s\n",
           #ifdef A
               "NO"
           #else
               "YES"
           #endif
            );
}
int main(){
    method1();
    method2();
    method3();
    return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  // file: snc.lex
%option noyywrap
%{
int comm = 0;
%}
%x STRING COMMENT
%%
<INITIAL>{
    \"         { BEGIN(STRING); ECHO; }
    "/*"       { comm++; BEGIN(COMMENT); }
    .          { ECHO; }
}
<COMMENT>{
    "/*"   { comm++; }
    .      { }
    "*/"   { comm--; if (!comm) BEGIN(INITIAL); }
}
<STRING>{
    \"     { BEGIN(INITIAL); ECHO; }
    .      { ECHO; }
}
%%
int main()
{
    yylex();
    return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY: all
all: nest_yes nest_no
	@echo "With nested comments support:"
	./nest_yes
	@echo "Without nested comments support:"
	./nest_no
 
nest_yes: nestcomm.c snc
	./snc < $< > tmp.c
	cc -o $@ tmp.c
	rm -f tmp.c
 
snc: snc.lex	
	flex -o $(subst .lex,.c,$<) $<
	cc -o $@ $(subst .lex,.c,$<)
 
nest_no: nestcomm.c
	cc -o $@ $<
 
.PHONY: clean
clean:
	rm -f snc.c snc nest_yes nest_no

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

УжасноПлохоНормальноХорошоОтлично (2 голосов, средний: 4,50 из 5)
Loading ... Loading ...

Удивительное рядом

Уличил сам себя в бессовестном разбазаривании вычислительных ресурсов. У меня запущен urxvt, в котором запущен tmux, в котором через ssh запущен screen, в котором запущен minicom, в котором видна консоль суровой отечественной железки. Остается только запустить это все в виртуальной машине.