Я регулярно пишу статьи о программировании и выступаю на конференциях. В ходе этого я просматриваю большое количество кода и ошибок. У меня был случай, когда в процессе написания статей, я выявил интересную закономерность, которую назвал "Эффект последней строки". Сейчас у меня дежавю. Я готов выделить ещё один паттерн ошибки, но с ним не всё так понятно, и мне интересно услышать мнение сообщества программистов.
Не так давно я делал две презентации (одну для C++, другую для C# программистов). Вдруг я понял, что в обоих случаях мне часто встречаются проблемы в небольших функциях сравнения, носящих такие имена как Compare, Cmp, Equal и так далее. При чем этот паттерн не зависит от языка. Я начал просматривать имеющуюся у меня коллекцию ошибок, и мои подозрения подтвердились. Теперь я могу заявить следующее:
Программисты часто допускают глупые ошибки в простых функциях, сравнивающих два объекта.
Однако, с объяснением этого эффекта не всё так прозрачно как в случае с "Эффектом последней строки". Тот эффект я могу объяснить тем, что ослабляется внимание. Почему же так много ошибок в функциях сравнения? Моё предположение: программисты считают такие функции простыми и однотипными и соответственно пишут их очень быстро, не удосуживаясь проверить. Они уверены, что функции просты, а значит программистам и в голову не приходит их проверять.
Я не знаю, прав я или нет. Мне очень интересно услышать, что думаю другие разработчики о замеченном эффекте. Думаю, это поможет написать мне интересную статью.
Чтобы не быть голословным, я приведу несколько примеров ошибок. Начнем с самых показательных:
public static int Compare(SourceLocation left,
SourceLocation right) {
if (left < right) return -1;
if (right > left) return 1;
return 0;
}
У меня много накопилось примеров, и в статье будет бОльшее количество. Сейчас приведу только некоторые, чтобы вопрос не был слишком длинный. Поверьте, раз я обратил на такие функции внимание, в этом во всём действительно есть какая-то закономерность.
Здравствуйте, Analytic2007, Вы писали:
A> const struct server_id *i1 = (struct server_id *)p1; A> const struct server_id *i2 = (struct server_id *)p2;
Это компилируется вообще? const void* переводится в T* без константы, это как?
Ну и убоищный структ перед названием типа.
Сдаётся мне, это сишка, а вовсе не кресты.
Re: Почему люди часто ошибаются при написании простых функций сравнения?
Здравствуйте, Analytic2007, Вы писали:
A>Программисты часто допускают глупые ошибки в простых функциях, сравнивающих два объекта.
A>Однако, с объяснением этого эффекта не всё так прозрачно как в случае с "Эффектом последней строки". Тот эффект я могу объяснить тем, что ослабляется внимание. Почему же так много ошибок в функциях сравнения? Моё предположение: программисты считают такие функции простыми и однотипными и соответственно пишут их очень быстро, не удосуживаясь проверить. Они уверены, что функции просты, а значит программистам и в голову не приходит их проверять.
Кладут болт на юнит-тесты, ваш КО.
Я не сторонник стремиться покрыть юнит-тестами весь код, но это один из тех случаев, когда они незаменимы.
Re: Почему люди часто ошибаются при написании простых функций сравнения?
В данном коде я вижу сразу две проблемы: во-первых используется странное соглашение, что функция compare может возвращать только -1, 0 и 1, а не <0, 0, >0 (проблема библиотеки). Если убрать это странное соглашение, то последние строки можно заменить на
return i1->pid - i2->pid;
а ещё лучше на
return Integer.compare(i1->pid, i2->pid);
Если же апгрейдить наш язык, добавив в него лямбды (ну и generics, конечно), эту функцию можно было бы записать как
Здравствуйте, Analytic2007, Вы писали:
A>Чтобы не быть голословным, я приведу несколько примеров ошибок.
Я правильно понял, что все ошибки имеют одну и ту же механику — скопировали кусок кода, что-то поправили, но не все или не совсем правильно?
Если это так, то эффект ожидаем. Когда человек пишет код или просто текст, он мысленно проговаривает его, осмысливает.
При операциях копирования и вставки такое осмысление не происходит. Набор слов и операторов не складываются в голове в единую логическую конструкцию.
Поэтому правки кода после копипасты зачастую неосмысленные.
С другой стороны маленькие функции, которые должны быть очень простые, не вызывают пристального внимания читающих. Мелкие ошибки остаются незамеченными очень долго.
Re[2]: Почему люди часто ошибаются при написании простых функций сравнения?
G>Я правильно понял, что все ошибки имеют одну и ту же механику — скопировали кусок кода, что-то поправили, но не все или не совсем правильно?
Скорее всего именно так.
G>Если это так, то эффект ожидаем. Когда человек пишет код или просто текст, он мысленно проговаривает его, осмысливает. G>При операциях копирования и вставки такое осмысление не происходит. Набор слов и операторов не складываются в голове в единую логическую конструкцию. G>Поэтому правки кода после копипасты зачастую неосмысленные.
G>С другой стороны маленькие функции, которые должны быть очень простые, не вызывают пристального внимания читающих. Мелкие ошибки остаются незамеченными очень долго.
Согласен. Сам склоняюсь к таким мыслям.
Re: Почему люди часто ошибаются при написании простых функций сравнения?
Мне кажется, что это частный случай более общей закономерности "интуитивно очевидные утверждения чаще оказываются неверными, чем утверждения сомнительные".
Сомнительные утверждения мы проверяем; интуитивно очевидные принимаем на веру.
Так и тут — для двухстрочных функций никто не озаботился написанием тестов и вообще мало-мальским доказательством корректности.
Вот если бы там было что-то посложнее, то наверняка программист бы не рискнул коммитить просто так, а задался бы либо внимательной вычиткой, либо peer review, либо нормальным тестовым покрытием.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re: Почему люди часто ошибаются при написании простых функций сравнения?
Здравствуйте, Analytic2007, Вы писали:
A>Программисты часто допускают глупые ошибки в простых функциях, сравнивающих два объекта.
A>Однако, с объяснением этого эффекта не всё так прозрачно как в случае с "Эффектом последней строки". Тот эффект я могу объяснить тем, что ослабляется внимание. Почему же так много ошибок в функциях сравнения? Моё предположение: программисты считают такие функции простыми и однотипными и соответственно пишут их очень быстро, не удосуживаясь проверить. Они уверены, что функции просты, а значит программистам и в голову не приходит их проверять.
Написать аксиоматически верный компаратор, кроме лексикографического, — вообще муторное дело.
А лексикографический — там будет или магия (макросы, шаблоны, лямбды, трюки), либо копипасты (многажды — имена объектов, четырежды — каждое поле).
В магии легко облажаться, да и код на грани write only. (Это же всё велосипеды большей или меньшей степени затейливости).
В копипастах легко очепятаться.
Были бы почленные операторы сравнения из коробки (как в хаскеле удобно: data Something = ...... deriving Cmp) — была бы сказка.
Была бы троичная логика из коробки — опять же была бы сказка.
Но сказки, увы, нет.
И самое главное, что средств верификации — из коробки тоже нет, а покрывать тестами вместо верификации — ну... непросто.
Перекуём баги на фичи!
Re: Почему люди часто ошибаются при написании простых функций сравнения?
Здравствуйте, Analytic2007, Вы писали: A>Не так давно я делал две презентации (одну для C++, другую для C# программистов). Вдруг я понял, что в обоих случаях мне часто встречаются проблемы в небольших функциях сравнения, носящих такие имена как Compare, Cmp, Equal и так далее. При чем этот паттерн не зависит от языка. Я начал просматривать имеющуюся у меня коллекцию ошибок, и мои подозрения подтвердились. Теперь я могу заявить следующее: A>Программисты часто допускают глупые ошибки в простых функциях, сравнивающих два объекта.
Кстати, говоря о таком коде хочу заметить, что множество этих ошибок можно было бы избежать за счёт правильного стиля оформления кода.
Я, например, придерживаюсь нижеизложенных правил и практически не допускаю ошибок на такие сравнение, за исключением проверок на равенство. Иногда путаю != с ==.
Итак, правила:
— однотипный код должен быть оформлен в таблицу;
— открывающая и закрывающая скобки должны быть либо в одной строке, либо в одном столбце;
— знаки сравнения > и => не используются;
— цифры не используются в идентификаторах.
Запишем код в соответствии с правилами: A>
A> if (i1->pid < i2->pid) return -1;
A> if (i2->pid > i2->pid) return 1;
A>
Заменяем 1 и 2 на Left и Right, знак > на <. Получаем:
A> if ( iLeft ->pid < iRight->pid ) return -1;
A> if ( iRight->pid < iRight->pid ) return 1;
При такой записи даже думать не надо — сразу видно нарушение симметрии.
Тут тоже самое:
A> if (left < right) return -1;
A> if (right > left) return 1;
заменяем знак > на <. Получаем:
A> if ( left < right ) return -1;
A> if ( left < right ) return 1;
A>С++ (MySQL) Длинный, но однотипный код.
Должен быть в виде таблицы:
static int
A>nsattcmp(const void *p1, const void *p2)
A>{
A> const XML_Char *att1 = *(const XML_Char **)p1;
A> const XML_Char *att2 = *(const XML_Char **)p2;
A> int sep1 = (tcsrchr(att1, NSSEP) != 0);
A> int sep2 = (tcsrchr(att1, NSSEP) != 0); /// <<<
A> if (sep1 != sep2)
A> return sep1 - sep2;
A> return tcscmp(att1, att2);
A>}
A>
Но, в некоторых случаях стиль не спасает, как, например, тут:
Здравствуйте, Кодт, Вы писали:
К>Написать аксиоматически верный компаратор, кроме лексикографического, — вообще муторное дело. К>А лексикографический — там будет или магия (макросы, шаблоны, лямбды, трюки), либо копипасты (многажды — имена объектов, четырежды — каждое поле).
К>В магии легко облажаться, да и код на грани write only. (Это же всё велосипеды большей или меньшей степени затейливости). К>В копипастах легко очепятаться.
К>Были бы почленные операторы сравнения из коробки (как в хаскеле удобно: data Something = ...... deriving Cmp) — была бы сказка. К>Была бы троичная логика из коробки — опять же была бы сказка. К>Но сказки, увы, нет.
Здравствуйте, PM, Вы писали:
К>>В магии легко облажаться, да и код на грани write only. (Это же всё велосипеды большей или меньшей степени затейливости).
PM>В С++11 можно немного приблизиться к сказке, перечислив члены структуры для компаратора только один раз, а затем использовать в компараторе указатели на эти члены: http://playfulprogramming.blogspot.ru/2016/01/a-flexible-lexicographical-comparator.html
См.выше про магию...
Я таких способов штук шесть накидать могу (и, собственно, хотел запостить, но потом стёр, как Гоголь второй том).
С кортежами и вариадиками вообще прекрасно:
template<class Obj, class... Members>
bool memberwise_less(const Obj& a, const Obj& b, Member... members) {
return std::tie(a.*members...) < std::tie(b.*members...); // tie, а не make_tuple, чтобы по ссылкам - чтобы не копировать
}
Но ведь потом захочется прекрасного: сравнения полей по ключам, т.е.
потом вспомнить о ленивости и вернуться к рекурсивным шаблонам
template<class Obj>
bool keyfun_less(const Obj& a, const Obj& b) { return false; }
template<class Obj, class KeyFun, class... KeyFuns>
bool keyfun_less(const Obj& a, const Obj& b, KeyFun key, KeyFuns... keys) {
{
auto&& ka = key(a);
auto&& kb = key(b);
if (ka < kb) return true;
if (kb < ka) return false;
} // отбрасываем временные переменныеreturn keyfun_less(a, b, keys...);
}
И, в итоге, получим какую-нибудь новую библиотечку в составе буста. Заодно, прибегнув к препроцессору, сможем родить код, совместимый даже с C++03. (А кстати, нет ли среди него чего-нибудь уже готового?)
Перекуём баги на фичи!
Re[4]: Почему люди часто ошибаются при написании простых функций сравнения?
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, PM, Вы писали:
К>>>В магии легко облажаться, да и код на грани write only. (Это же всё велосипеды большей или меньшей степени затейливости).
PM>>В С++11 можно немного приблизиться к сказке, перечислив члены структуры для компаратора только один раз, а затем использовать в компараторе указатели на эти члены: http://playfulprogramming.blogspot.ru/2016/01/a-flexible-lexicographical-comparator.html
К>См.выше про магию... К>Я таких способов штук шесть накидать могу (и, собственно, хотел запостить, но потом стёр, как Гоголь второй том).
Понятно. Мой персональный выбор — использовать std::tie, потому что в 99% случаев требуется лишь operator== и operator< для структуры с несколькими полями.
К>И, в итоге, получим какую-нибудь новую библиотечку в составе буста. Заодно, прибегнув к препроцессору, сможем родить код, совместимый даже с C++03. (А кстати, нет ли среди него чего-нибудь уже готового?)
Есть boost.fusion как заменитель compile-time reflection. Адаптировав для fusion структуру, можно в том числе получить и почленное сравнение: