Давеча один из интересующихся SObjectizer-ом пользователей задал вопрос: а можно ли использовать в SO-5 в качестве обработчиков сообщений const-методы? Вопрос оказался, что называется, внезапным. Мы используем SO-5 в разработке софта уже лет семь (как раз где-то в конце 2010 первые стабильные версии SO-5 пошли в дело, если мне не изменяет склероз). Но ни разу нам не пришлось столкнуться с тем, чтобы обработчик сообщения следовало бы пометить модификатором const. Видимо, это потому, что обработчики сообщений в подавляющем большинстве случаев меняют состояние агента, поэтому не было смысла использовать const-методы в качестве обработчиков. А тут новый человек, другие задачи, другой взгляд на решение этих задач. И const-методы оказались нужны.
Ну, OK. Какие проблемы, будем делать.
Только вот как это сделать так, чтобы трудоемкость и сопровождаемость получившегося решения не зашкалила? Речь идет о том, чтобы преобразовать 6-7 методов вот такого формата:
template< class RESULT, class ARG, class AGENT > subscription_bind_t & event( RESULT (AGENT::*pfn)( ARG ), thread_safety_t thread_safety = not_thread_safe ); |
которые живут в SO-5 давно, и используются повсеместно.
Нужно было получить методы, которые бы могли принять как RESULT(AGENT::*pfn)(ARG), так и RESULT(AGENT::*pfn)(ARG) const.
Простейший вариант, который приходит в голову -- это тупое дублирование:
template< class RESULT, class ARG, class AGENT > subscription_bind_t & event( RESULT (AGENT::*pfn)( ARG ), thread_safety_t thread_safety = not_thread_safe ); template< class RESULT, class ARG, class AGENT > subscription_bind_t & event( RESULT (AGENT::*pfn)( ARG ) const, thread_safety_t thread_safety = not_thread_safe ); |
Но копипаста -- это не наш метод! (Я несколько раз в начале своей карьеры набивал здоровенные шишки из-за обилия копипасты и зарекся использовать этот способ вне зависимости от ситуации. Чем сильно облегчил себе жизнь).
Поэтому оставался путь использования шаблонов. Чтобы был всего один набор методов, куда указатель на метод передавался бы как шаблонный параметр:
template< class Method_Pointer > subscription_bind_t & event( Method_Pointer pfn, thread_safety_t thread_safety = not_thread_safe ); |
Но засада была в том, что методы с такой сигнатурой уже использовались для работы с лямбдами (т.е. для ситуаций, когда обработчик события задается лямбдой).
Соответственно, чтобы отделить методы для работы с указателями, от методов для работы с лямбдами, пришлось прибегнуть к SFINAE:
template< typename Method_Pointer > typename std::enable_if< details::is_agent_method_pointer<Method_Pointer>::value, subscription_bind_t & >::type event( Method_Pointer pfn, thread_safety_t thread_safety = not_thread_safe ); template< class Lambda > typename std::enable_if< details::lambda_traits::is_lambda<Lambda>::value, subscription_bind_t & >::type event( Lambda && lambda, thread_safety_t thread_safety = not_thread_safe ); |
Что, в совокупности с написанными для этих целей классов-предикатов is_agent_method_pointer и is_lambda, изрядно усложнило код SObjectizer-а.
А это заставило задуматься о том, а оправданная ли это сложность? Не является ли такая сложность всего лишь борьбой со сложностью самого языка? А не борьбой со сложностью решаемой задачи...
Непростой вопрос.
Думается, что здесь одно цепляется за другое. Например, само наличие const-методов -- это же было усложнение ЯП в то время, когда const-методы в C++ появились (появились, кстати, не сразу, а спустя пару лет после публичного релиза языка). Мне, например, достаточно сложно вспомнить более-менее известный и близкий к мейнстриму язык, в котором можно было бы создавать константные объекты и в compile-time контролировать то, какие методы у этого объекта могут быть вызваны (функциональные языки не берем, у них иммутабельность -- это вообще краеугольный камень). Вспоминаются разве что D и Rust. Ни в Eiffel (ровесник C++), ни в Java (на десять лет моложе), ни в первых версиях C# (на 15 лет моложе), ни в Scala (почти в два раза моложе) нет const-методов, ни во многих последующих языках ничего подобного нет.
Т.е. в этом конкретном аспекте C++ сложнее многих других распространенных языков. Но эта сложность более чем окупается, на мой взгляд, так как еще во время компиляции позволяет защититься от глупых ошибок. Причем, имхо, такая защита стала только актуальнее с момента вступления в эру многоядерных процессоров и многопоточности. И хотя const в C++ -- это всего лишь const-view, а не строгая гарантия иммутабельности, но все равно это лучше, чем полное отсутствие const-антности.
Ну а раз эта сложность оказалась в языке не просто так, а для упрощения жизни нам, разработчикам, то с последствиями этой сложности приходится считаться. Вот как в описанном случае. Что и приводит к усложнению кода в библиотеках, когда приходится одновременно поддерживать const- и не-const-методы (а ведь есть еще и volatile...).
В общем, получается замкнутый круг: сложность возникает, чтобы дать дополнительные возможности и/или гарантии разработчику. Затем сложность увеличивается за счет того, что нужно учитывать ранее возникшую сложность. Так и возникают возможности, которые решают проблемы не конечных пользователей языка, которым, скажем, нужен быстрый и экономный код для работы на слабом девайсе. А проблемы разработчиков библиотек, упрощающих жизнь конечных пользователей языка, т.к. за счет библиотек можно разработать быстрый и экономичный код. Но усложняется же при этом сам язык. И насколько это усложнение отталкивает от языка конечных пользователей, которым нужно просто писать быстрый и экономичный код... Вот это уже другой вопрос. Судя по интересу к тому же Go, в настоящее время такие параметры, как time-to-market и возможность посадить на проект зеленого джуниора, значительно перевешивают все остальное. Впрочем, это уже совсем другая история.
PS. Ну а концепты в C++, действительно, выглядят далеко не лишними. Хорошо, что они попали в C++20. Плохо, что до этого еще нужно дожить... :(
Комментариев нет:
Отправить комментарий