Как стать автором
Обновить

Комментарии 49

2011-й год ознаменовался резким повышением коэффициента тортности Хабра!

Честно говоря, «шаблонная магия» — не самый сильный мой скилл, но прочитал с интересом и огромным удовольствием. Спасибо!
Что у трезвого на уме, то пьяный уже запостил?
На что люди только не идут, чтобы не использовать Python (:
Для подобной мути лучше использовать кодогенераторы (к примеру, moc в Qt). Гораздо более естественно получается.

Шаблоны в С++ — это тоже, в принципе, такой своеобразный кодогенератор, но его использование даёт больше проблем, чем решает (ошибки компиляции при участии шаблонов могут временами вызывать ужас при одном лишь взгляде на них).
В реальных проектах использование шаблонов, как правило, практически полностью ограничено классами-контейнерами.
Согласен, ошибки компиляции при использовании шаблонов это то еще зло :)

По поводу кодогенерации, то в проекте она есть, принцип следующий: сначала генерируем тип шаблонами, затем вызываем специальную программку, которая выводит в .cpp файлы typeid( TGeneratedType ).name() в нужных местах, это одним махом раскрывает все метавызовы под отладкой жить становится значительно проще :)

Возможно, когда-нибудь я перейду таки на полноценную кодогенерацию в таких местах…
Есть, кстати, еще код, который для создания объектов сначала генерирует xml, а потом делает «сериализацию», в этом случае все стратегии, меняющие поведение объектов компилируются не статически, как в случае шаблонов, а через указатели на интерфейсы.

Но в данном случае это очень медленно и дорого.
Есть конфиги sendmail'а, а есть код, написанный с использованием boost::mpl
Похоже новогодние праздники стимулируют мозговую активность и желание делится у программистов на с++.))
Насчет мозговой активности не согласен :D
Неправда, мне уже ни делиться, ни любым другим образом размножаться уже не хочется. Хочется в спору превратиться, только бы их пережить.
лишние врапперы, хотя и не наносили вреда логике работы, но увеличивали размер объектов
Бред! Если в классе уже есть хотя бы один виртуальный метод, что автоматически подразумевается, коль в посте упоминается фабрика объектов, то создание дополнительных виртуальных методов не увеличит размер экземпляра. А накладные расходы на лишний пустой косвенный вызов просто несущественны перед ужасом, описанном в посте.

И я так понимаю, принципу KISS следовать нынче немодно? Нужно, чтоб обязательно метапрограммирование любой ценой и компиляция часами? Фи!
Не судите строго, это такой «синдром Александреску» после прочтения его книги :) Поражает 98% С++ программистов. Почти все в своей жизни писали нечто такое вот монструозное, что бы потом, через время, удалить это всё со словами «экая знатная хренотень получилась».

Каждый, кто впервые сталкивался с его магией списков типов норовил впихнуть их везде, где только можно, в надежде «впихнуть невпихуемое», то есть совместить статику и динамику С++.
Главное это то, на что люди идут, ради того, что бы сделать С++ динамическим. Глядя на все эти, безусловно, «гениальные» построения, монструозные горы шаблонов, прям душа радуется :)

Самое первое правило от одного из отцов гласит «если вам понадобилась информация о типах, значит вы что-то не так спроектировали». Но многие люди хотят сделать чёткую автоматическую связку идентификатор-тип и, таким образом, привнести динамику в статический мир С++. Вот это печально ;)
Но со временем всё равно приходит понимает, что шаблоны — это метапрограммирование, и как бы там ни было эта штука работает на стадии компиляции, а вот только потом уже рантайм.

Автору хотелось бы посоветовать после того, как отойдет эйфория от Александреску, покурить Буча, Гамму с корешами, Мейерса и других знатных дядек, которые уже лет эдак 5-10 назад нарисовали тучу простых и элегантных решений большинства стандартных проблем.
Вы сейчас говорите с точки зрения ООП.

ООП хорошо работает с целыми объектами. Шаблоны позволяют конструировать целые объекты из множества ПРОСТЫХ «деталек». Отдельно взятые «детальки» (врапперы, стратегии) целым объектом считать нельзя, они лишь реализуют какую-то определенную деталь поведения объекта. Чтобы объекты из этих деталек собирались нормально, а не впадали в «панику» из-за того, что кто-то скрепил детальки не в том порядке, в котором нужно, и используется информация о типах.

С точки зрения фабрики динамика означает лишь одно: на этапе инициализации вычислений мы можем проанализировать, какая функциональность потребуется от того или иного объекта, используя шаблоны мы можем собрать объект из «деталек», который будет нести в себе только эту функциональность, в противном случае все объекты были бы монстрами, способными обработать множество ситуаций, платой за что является либо память либо производительность.
Извините, но судя по тексту и комментарию у вас очень однобокое понимание ООП. Вы говорите об объектах, но забываете про типы, хотя это равноправные сущности ООП. Для вас важны лишь объекты, именно потому вы так отчаянно прыгаете от метатипов к объектам, минуя собственно типы в проектировании и реализации. Вы однобоко мыслите стратегиями и пытаетесь их везде впихнуть, ибо именно это описано у Александреску, но не знаете про остальные шаблоны проектирования (либо упускаете из виду). Вполне вероятно, для вашей задачи очень успешно подойдут другие решения и модели, которые будут и выглядеть элегантно, и работать шустро. В конце-концов, чистый С всё равно быстрее, а уж дискретная математика и методы оптимизации и подавно на С быстрее, чем горы любых классов.

В конце-концов, это ваше дело, пишите и попробуйте взглянуть на это ваше изделие через годик. Расскажите потом ваши впечатления.
Я даже не сомневаюсь, что через годик я буду думать совсем по другому, это нормально и означает развитие :)

> Вполне вероятно, для вашей задачи очень успешно подойдут другие решения и модели, которые будут и выглядеть элегантно, и работать шустро
90% кода там написано не мной и, более того, не фанатами шаблонов. Мне просто нужно было научить кучу объектов фильтровать поступающие данные, причем так, чтобы это не сказалось на производительности, когда соответствующая опция отключена. В текущей реализации в этой ситуации просто создается другой тип, который фильтровать ничего не умеет.

> минуя собственно типы в проектировании и реализации
Это относится уже особенностям реализации наших алгоритмов, я не имею права разговаривать на эту тему. Просто поверьте на слово, что и типы и другие подходы там есть в разных местах, но они не позволяли внедрить новую фичу во все классы сразу. Точнее позволяли, но так что потом «черт ногу сломит», новая функциональность стала бы размазанной по всему коду.
чистый си либо удобнее и медленее, либо быстрее, неудобнее и слишком много копипасты…
поищите в сети реализацию универсальной сортировки на С и С++, решение на шаблонах выигрывает из-за отсутствия вызовов функций по указателю…
Вот не нужно бреда :) Этот классический пример пихают уже с поводом и без повода. Шаблонная функция выигрывает из-за того, что она инлайнится, и то далеко не всегда. Но это не показатель крутизны шаблонов. Сделайте вручную на С то, что делает компилятор с шаблонами и вы получите скорость выше. Копипаста возникает из-за вашей же криворукости. Неудобство понятие очень растяжимое и субъективное, а так же сильно зависит от задачи, например, в ядре ОСи что-то плюсы не приживаются, тяжеловат-с.
Про 98% не согласен :)
В остальном верно.
> Бред! Если в классе уже есть хотя бы один виртуальный метод, что автоматически подразумевается, коль в посте упоминается фабрика > объектов, то создание дополнительных виртуальных методов не увеличит размер экземпляра. А накладные расходы на лишний пустой > косвенный вызов просто несущественны перед ужасом, описанном в посте.

Дело вовсе не в виртуальных вызовах, они там как были так и остались. Для своей работы врапперам нужны дополнительные данные, и эти данные кушают «лишнюю» память. В некоторых объектов оптимизация доходит до того, что double, который хранит проценты ужимается до 4х байт…
А что за задачи, и на какой платформе они выполняются?
Симуляция рисков страховых компаний методом Монте Карло.

Большие проекты симулируются несколько дней на нескольких компьютерах (уровня i7). Полученные данные хранятся внутри атомов (тех самых объектов) здоровенного гиперкуба. Количество атомов при некоторых условиях достигает 100-200 миллионов.
Я бы не советовал это делать, используя такое количество мелких объектов, выделяемых в куче.
Да я, уже давно хочу попробовать SmallObjectAllocator, смущает лишь одно: new оставляет за собой «заборчики» в отладочной версии, которые сильно помогают в поиске умерших не своей смертью объектов…

Кроме того, я не пытался анализировать, где там можно сэкономить память лично. Те оптимизации, которые в коде появлялись были результатом работы (не моей) над «хитрыми» проектами, которые присылали пользователи. Оптимизация double, например, была связана с резким ростом количества данных за счет комбинаторных перестановок параметров, которые вводил пользователь.

Ну и добавлю, что накладные расходы на враппер могут достигать нескольких сотен байт (для хранения «путей доступа к данным», если на пальцах). Кончено, этот путь можно было бы расчитывать во время обработки события, но это уже накладно по отношению к процессору. Компромисс — это включать враппер только там, где он реально нужен.
Да я имею в виду, что у вас каждый «объект» самостоятельно динамически выделяется. Это несколько неразумно, когда объектов — миллионы, а размер из — крохотный.

Представьте, что вы с изображениями работаете, к примеру. У вас есть цветные пикселы, черно-белые, серые, с прозрачностью и т.п. И вот, для каждого вида изображения вы выделяете миллионы объектов типа «bla-bla-bla пиксел». Оно тормозит, и вы прикручиваете как-бы оптимизацию, чтобы быстрее было. Мечтаете прикрутить «очень быстрый аллокатор», и т.п.

А проблема, вообще-то, лежит в другой плоскости.
В таком случае, перво-наперво надо урезать накладные расходы за счет избавления от shared_ptr, которые почти наверняка просто не нужны. Напоминаю, что shared_ptr — это по определению лишний микроблок на куче (N байт на заголовок; в glibc это вроде бы 12 байт), содержащий счетчик ссылок и указатель на объект (а вот и лишний слой косвенной адресации).

Но не в этом суть. Когда при живом динамическом полиморфизме начинаются выкрутасы «если экземпляр объекта является потомком класса», в мозгу должна вспыхивать красная лампочка «я что-то делаю неправильно».

В некоторых объектов оптимизация доходит до того, что double, который хранит проценты ужимается до 4х байт…
При увиденном мной ужасе это абсолютно копеечная экономия. Не говоря уже о том, что для хранения процентов с точностью до двух десятичных знаков после запятой достаточно всего двух байт. Самые хардкорщики вообще используют арифметику с фиксированной точностью и вписывают в те же два байта число от 0 до 255 с точностью до 1/256 после запятой.
> В таком случае, перво-наперво надо урезать накладные расходы за счет избавления от shared_ptr
Они там просто для примера, меня коробило писать IObject *pOject = new CObject().

> Но не в этом суть. Когда при живом динамическом полиморфизме начинаются выкрутасы «если экземпляр объекта является потомком класса», в мозгу должна вспыхивать красная лампочка «я что-то делаю неправильно».

Я приведу пример. Представьте у вас есть некоторый IObject, все что он реализует — это интерфейс для получения какого-либо значения. От него наследуются ICalculable и ISettable, первый никаких значений не хранит, высчитывая их динамически, воторой по честному хранит внутри себя некое число (вектор, матрицу). Для передачи значений в хранилище используется система событий, т.е. все ISettable подписываются на какие-либо источники событий. При определенных условиях события эти надо фильтровать, т.е. получать реально только те, которые соответствуют определенному критерию (и да, для этого нужно хранить еще и критерий). Для реализации этого поведения есть специальный враппер. Очевидно, что такой враппер можно применять только к тем объектам, которые унаследованы от ISettable. Т.е. если система решает, что в данном случае в проекте должны быть включены такие фильтры, то типы всех классов, которые унаследованы от ISettable нужно подменить новыми типами — с примененным враппером.

> При увиденном мной ужасе это абсолютно копеечная экономия.
Обоснуйте, там в коде, вообще нету ни одной переменной (про shared_ptr я ответил)
>При определенных условиях события эти надо фильтровать, т.е. получать реально только те, которые соответствуют определенному критерию (и да, для этого нужно хранить еще и критерий).
Что мешает завести в базовом классе виртуальный метод filter, который будет переопределяться в наследниках с тем, чтобы они работали лишь с нужными типами событий? Или я чего-то недопонял?

>Очевидно, что такой враппер можно применять только к тем объектам, которые унаследованы от ISettable.
Ок, помещаем метод filter в базовый класс нашей иерархии и задаем его поведение таким образом, что только в ISettable и его наследниках он занимается полезной деятельностью, а во всех остальных случаях возвращает false/ничего не делает. Или это порождает тонны копипасты в реализации этого метода в разнообразных наследниках?

>Обоснуйте, там в коде, вообще нету ни одной переменной (про shared_ptr я ответил)
Как минимум дублируется информация о типе объекта, которой компилятор и без того располагает. А дублирование такой информации всегда сопряжено с проблемами: добавляем новый класс в иерархию и велкам ту разбирательство, куда еще надо всунуть пару магических констант.
Я это говорю с точки зрения человека, которому приходилось читать такие вот шаблонные опусы и материться на «аффтаров», перечитавших Александрески и прочих упоротых шаблонных метапрограммистов. Намекну, что шаблоны — вещь очень не бесплатная, как минимум, по времени компиляции (доводилось видеть отдельные файлы, компилирующиеся по 20 минут).
Ах да, еще они крайне проблематичны в поддержке без внятной документации. Но «не читателей» это ведь, как обычно, не беспокоит?
> Что мешает завести в базовом классе виртуальный метод filter, который будет переопределяться в наследниках с тем, чтобы они работали лишь с
> нужными типами событий? Или я чего-то недопонял?

Вообще-то оно так и работает, type traits нужны чтобы определять, какой именно создавать класс (т.е. есть, если мы знаем, что данное событие происходить не будет из-за того, что не включена какая-то опция в настройках программы, то мы можем создавать те типы, которые будут не способны обрабатывать данное событие (а значит, не будет и накладных расходов на его обработку))

> Ок, помещаем метод filter в базовый класс нашей иерархии и задаем его поведение таким образом, что только в ISettable и его наследниках он
> занимается полезной деятельностью, а во всех остальных случаях возвращает false/ничего не делает. Или это порождает тонны копипасты в
> реализации этого метода в разнообразных наследниках?
1. Это очень странно так делать… ну представьте у вас есть класс «корабль» в котором из базового класса затесались функции управления элеронами (используются для самолета). Какие нафиг элероны в корабле?! :)
2. Даже если так сделать, это не решает проблему: враппер он на то и враппер, чтоб не писать лишний код, т.е. да фильтрацию можно было бы определить для каждого класса в отдельности… вот только это порядка 30 классов в которых будет ИДЕНТИЧНЫЙ код, поэтому враппер сделан как шаблонный класс, который можно «натравить» на то, что надо :) Однако, если враппер применить не к ISettable оно просто не скомпилируется (т.к. он пользуется функциями этого интерфейса).

> Как минимум дублируется информация о типе объекта, которой компилятор и без того располагает. А дублирование такой информации всегда
> сопряжено с проблемами:
Информацией о типе объекта компилятор располагает только в шаблонах. В динамике — только таблицей виртуальных функций. И вытащить из нее тип задача очень нетривиальная, а о передачи типа (именно типа!) из одной функции в другую в динамике я вообще молчу.

> добавляем новый класс в иерархию и велкам ту разбирательство, куда еще надо всунуть пару магических констант.
Как раз в том и фишка, что раньше надо было эти константы «всовывать», теперь не надо. Теперь ID типа генерируется автоматически. Все что нужно сделать — это зарегистрировать новый тип (это одна строчка), причем, если этого не сделать оно даже не скомпилируется.

> Я это говорю с точки зрения человека, которому приходилось читать такие вот шаблонные опусы и материться на «аффтаров», перечитавших
> Александрески и прочих упоротых шаблонных метапрограммистов. Намекну, что шаблоны — вещь очень не бесплатная, как минимум, по времени
> компиляции (доводилось видеть отдельные файлы, компилирующиеся по 20 минут).
Там есть хитрые оптимизации, шаблоны парсятся компилятором только один раз, потом генерируется исходник с «раскрытыми» (по возможности) шаблонами. Это дает 2 плюшки — во первых быстрее компилируется (на самом деле, что сейчас, что до того скорость компиляции приблизительно одинаковая, может на 1-2% изменилась), во вторых можно залезть в этот сгенерированный файл и там черным по белому написано какому типу, какой ID соответствует

> Ах да, еще они крайне проблематичны в поддержке без внятной документации. Но «не читателей» это ведь, как обычно, не беспокоит?
Документация есть
Не влезая в подробности, наличие switch со 100 case'ами означает что вы забыли реализовать некую иерархию субклассов.
Вот чтоб от switch избавится, это и было написано :)

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

Там, где она не нужна все выглядит значительно проще :)
Шаблоны C++ это мощный инструмент, но использовать его нужно с умом, иначе черт голову сломит!
Шаблонный код «сложно» писать, зато легко использовать :)

Посмотрите, например, реализацию boost.

«Сложно» взято в кавычки потому, что сложно только в том случае, если Вы его никогда не писали. Там думать надо немного по-другому :)
>Шаблонный код «сложно» писать, зато легко использовать :)

Шаблонный код легче писать, чем потом читать и использовать.
Александреску, например, показал не то, как нужно использовать шаблоны, а что с их помощью можно сделать. Так что перед тем, как выдать шаблон с 5 шаблонными параметрами, нужно подумать, а действительно ли это нужно.
Я слабо верю в то, что boost пишут люди далекие от программирования, между тем это ведь одни сплошные шаблоны.
когдато давным давно мы использовали такой чит, для получения уникального int по типу:

template &lt class A >
class GetType{
static int get_type(){
return (int)( &get_type() );
};
};

:)
Красиво, надо будет попробовать :)
некоторые современные компиляторы такое уже запрещают :)
Для этого есть адекватная причина? Я ярых противоречий стандарту не вижу…
увы я уже не помню почему, надо разгребать рабочую переписку
Как минимум sizeof(int) может быть меньше sizeof(int (*)()), с вытекающей потенциальной неуникальностью иднтификаторов. Но int можно заменить на intptr_t.
Ну это понятно.
Хотя, к слову, неуникальности не будет даже с int (размер исполняемого файла вряд ли будет больше 4Гб)
тьфу скобки тут
return (int)( &get_type() );
конечно лишние, там просто указатель берется от функции.
return (int)( &get_type );
Очень быстро запутался, и смог осилить статью лишь фрагментарно.

Я правильно понимаю, что вы по сути делаете свой, быстрый dynamic_cast?
Нет не правильно…

Если на пальцах:

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

Таким образом, появляются 2 следующие возможности:

1.
Оперировать типами как обычными переменными, вы можете передать куда угодно тип (точнее его целочисленный идентификатор) во время выполнения программы, при этом объект данного типа вообще не создается (его, может быть, и создать-то нельзя). Получив такое значение вы можете полностью восстановить скрытый под ним тип, как будто вы «угадали» нужный typedef (при чем ваш код сам по себе не обязан ничего знать о том, какие именно типы вам могут передать). После этого вы можете делать с этим типом все что угодно: модифицировать, анализировать, и т.д.

В качестве примера (сильно надуманного):
Можно написать функцию, которая, перебрав все возможные типы объектов (перебрав идентификаторы типа от 0 до N), выведет на экран диаграмму наследования для этих типов.
Когда вы добавите любой новый тип, эта функция его подхватит и будет выводить уже новую диаграмму, при этом в самой функции вам менять ничего не придется. Все что вам нужно будет сделать, это зарегистрировать ваш тип в списке известных типов (просто дописать туда его имя). Никаких функций в самом классе при этом писать не надо.

2.
Когда объект (полиморфный) уже создан вы можете у него узнать его реальный идентификатор типа, который позволяет полностью вывести его реальный тип (что, например, позволяет сделать static_cast до «the most derived» типа — это к вопросу о dynamic_cast)
template< class TFunctor >
inline void CallWithType( const TFunctor &rcFunctor, int nTypeDescriptor );

...

template< class TObject >
inline bool IsKindOf( int nTypeDescriptor )
{
    return CallWithType( IsKindOfHelper< TObject >(), nTypeDescriptor );
}

Возвращаемые значения IsKindOf и CallWithType не соответствуют друг другу.

// TypeDescriptor is virtual func, which returns descriptor of the real object type.
// Implementation shown in previous example (you don't need to write this function by hands)
if( IsKindOf< ISettable >( TypeDescriptor() ) )

Если TypeDescriptor виртульная функция, то где имя объекта?

 // IsWrapperApplicable converts nType to the real type TObject using CallWithType,
            // calls WrapperTraits::CSomeWrapper::MakeWrappedType< TObject >::type metafunction
            // in order to resolve wrapped type, then calls MakeDescriptor for this type.
            return MakeWrappedType< WrapperTraits::CSomeWrapper >( nType ) );

Последняя закрывающая скобка — лишняя.
Исправил, спасибо!
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории