Рассмотрим объект функции F
, принимающий constexpr size_t
аргумент I
struct F
{
template <size_t I>
constexpr size_t operator()(size <I>) const { return I; }
};
завернутый в тип size <I>
, где (для краткости)
template <size_t N>
using size = std::integral_constant <size_t, N>;
Конечно, мы могли бы передать I
напрямую, но я хочу подчеркнуть, что это constexpr, используя его в качестве аргумента шаблона. Функция F
здесь является фиктивной, но на самом деле она может делать множество полезных вещей, таких как извлечение информации из I
th элемента кортежа. Предполагается, что F
имеет один и тот же тип возврата независимо от I
. I
может быть любого целочисленного типа, но считается неотрицательным.
Проблема
Учитывая значение constexpr size_t
I
, мы можем вызвать F
с помощью
F()(size <I>());
Теперь, что, если мы хотим вызвать F
с не-constepr size_t
значением i
? Рассмотрим следующее:
constexpr size_t L = 10;
idx <F, L> f;
for (size_t i = 0; i < L; ++i)
cout << f(i) << " ";
(Зачем мне это нужно? Чтобы дать некоторый контекст, я на самом деле пытаюсь встроить составной итератор в представление контейнера, которое представляет собой последовательность "соединенных" (конкатенированных) разнородных контейнеров. Это дало бы возможность сказать что-то вроде join(a, b) = c;
, где массивы join(a, b)
и c
имеют одинаковую длину. Однако i
является состоянием итератора, поэтому не может быть constexpr
, но суб-итераторы хранятся в кортеже, и к ним нужно обращаться по индексу constexpr
. Отдельные value_type
примерно согласованы, так что объединенное представление может принимать их тип common_type
, но вложенные контейнеры и, следовательно, вложенные итераторы имеют разные типы.)
Решение
Здесь я придумал структуру idx <F, L>
, которая адаптирует функцию F
для этой цели, предполагая, что входной аргумент меньше L
. Это на самом деле прекрасно компилирует вывод
0 1 2 3 4 5 6 7 8 9
и вот живой пример.
idx
работает путем рекурсивной декомпозиции входных данных i
в двоичное представление и реконструкции аналога constexpr N
:
template <typename F, size_t L, size_t N = 0, bool = (N < L)>
struct idx
{
template <size_t R = 1>
inline constexpr decltype(F()(size <0>()))
operator()(size_t I, size <R> = size <R>()) const
{
return I%2 ? idx <F, L, N+R>()(--I/2, size <2*R>()) :
I ? idx <F, L, N >()( I/2, size <2*R>()) :
F()(size <N>());
}
};
где R
представляет степень 2
на текущей итерации. Чтобы избежать бесконечного создания экземпляров шаблона, для N >= L
дается специализация, возвращающая F()(size <0>())
в качестве фиктивного значения:
template <typename F, size_t L, size_t N>
struct idx <F, L, N, false>
{
template <size_t R>
inline constexpr decltype(F()(size <0>()))
operator()(size_t I, size <R>) const { return F()(size <0>()); }
};
По сути, этот метод является обобщением более распространенной идиомы с логическим аргументом:
bool b = true;
b ? f <true>() : f <false>();
где f
— функция, принимающая bool
в качестве аргумента шаблона. В этом случае очевидно, что реализованы все две возможные версии f
.
Вопрос
Хотя это работает, и его сложность во время выполнения предположительно логарифмическая в i
, меня беспокоят последствия во время компиляции, например:
сколько комбинаций
idx
и егоtemplate operator()
создается для того, чтобы это работало во время выполнения для любого вводаi
, который не известен во время компиляции? (я понимаю опять "все возможные", но сколько?)действительно ли возможно встроить
operator()
?есть ли какая-либо альтернативная стратегия или вариант, который «легче» скомпилировать?
должен ли я забыть об этой идее как о случае чистого раздувания кода?
Примечания
Вот время компиляции (в секундах) и размер исполняемого файла (в КБ), которые я измерил для разных значений L
:
L Clang(s) GCC(s) Clang(KB) GCC(KB)
10 1.3 1.7 33 36
20 2.1 3.0 48 65
40 3.7 5.8 87 120
80 7.0 11.2 160 222
160 13.9 23.4 306 430
320 28.1 47.8 578 850
640 56.5 103.0 1126 1753
Таким образом, хотя в L
он кажется примерно линейным, он довольно длинный и разочаровывающе большой.
Попытка форсировать встроенный operator()
не удалась: вероятно, Clang проигнорировал ее (исполняемый файл даже больше), в то время как GCC сообщает recursive inlining
.
Запуск nm -C
в исполняемом файле, например. для L = 160
показывает 511/1253
разные версии operator()
(с Clang/GCC). Это все для N < L
, поэтому похоже, что конечная специализация N >= L
действительно встроена.
ПС. Я бы добавил тег code-bloat
, но система не пускает.
L
когда-либо будет больше 5? - person ecatmur   schedule 21.02.2014