Снижение раздувания шаблонов C++ за счёт выделения зависящих от типа частей функции

Шаблоны C++ позволяют повторно использовать код, но это имеет свою цену: каждое развёртывание шаблона порождает отдельную функцию. Для небольших функций это не так критично, но чем сложнее становится функция, тем выше стоимость множащихся развёртываний.

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

Иногда я вижу большие шаблонные функции, в которых очень мало зависимостей от типов. В этом крайнем случае единственной зависимостью от типа является Table::Columns(). (Более распространённым источником зависимостей по типу были бы вызовы методов у параметра, являющегося шаблонным.)

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

Один из способов смягчить взрыв количества кода — обернуть все общие части в вспомогательный объект. Теперь разные развёртывания функции something могут разделять конструктор SomethingState и его методы, так что уникальные функции становятся довольно маленькими. Мы помечаем конструктор SomethingState и его методы как «no-inline», чтобы отговорить компилятор от их инлайна, потому что инлайнинг подорвал бы наше выделение общих частей. Смежная тема: noinline inline функция? Какая-то чародейская штука?

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

Хитрость в этом подходе — найти некоторый общий тип, который разделяют все развёртывания. Предположу, что Table::Columns() — это C-style массив объектов Column, или std::vector объектов Column, или std::array объектов Column, или в общем нечто, из чего можно получить std::span объектов Column.

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

не происходит взрыва шаблонов.

Одно, что следует учесть — мы меняем порядок вычислений. Старый код не вызывал Table::Columns() до завершения подготовительных шагов. Можно посмотреть код, чтобы убедиться, но я предполагаю, что Table::Columns() просто возвращает ссылку на некоторый заранее существующий источник информации о столбцах, так что не имеет значения, когда вы его вызываете. Даже если он возвращал столбцы по значению (например, клонируя внутренний вектор), ранний запрос столбцов действительно меняет момент, в который этот вектор создаётся, но создание его заранее, даже если подготовительные шаги завершатся ошибкой, скорее всего не проблема, потому что (1) создание не вызывает никаких интересных побочных эффектов, (2) порядок вычислений не критичен, и (3) случай неудачи, вероятно, редок, так что дополнительные затраты на генерацию вектора, который затем не будет использован, несуществены.

Мы применим эти принципы к нашему предыдущему примеру и сделаем удивительное открытие, которое шокирует и поразит вас.

Рэймонд участвует в развитии Windows более 30 лет. В 2003 году он начал веб-сайт под названием The Old New Thing, который стал популярнее, чем он когда-либо мог представить, развитие чего до сих пор вызывает у него нервное волнение. Веб-сайт породил книгу, также названную The Old New Thing (Addison Wesley, 2007). Иногда он появляется в аккаунте Windows Dev Docs в Twitter, чтобы рассказывать истории, не содержащие полезной информации.

Сокращение раздутия C++ шаблонов выносом зависящих от типов частей функции, практический экзамен.

В финале продуктовой игры каждое изменение несёт значительные риски, эпизод 2.