Я регулярно сталкиваюсь с ситуациями в моем коде, в которых я хотел бы выполнять итерации по диапазону в прямом или обратном порядке в зависимости от условий выполнения. Это обычно приводит к коду, подобному следующему
if (reverse) {
using boost::adaptors::reversed;
for (auto const & x : range | reversed) do_stuff(x);
} else {
for (auto const & x : range) do_stuff(x);
}
или же
std::vector<Foo> v(range.begin(), range.end());
if (reverse) boost::range::reverse(v);
for (auto const & x : v) do_stuff(x);
который содержит дублирование кода (первый) или неэффективен (второй).
Я много раз задумывался о гипотетическом адаптере диапазона Boost, который мог бы условно изменить диапазон, чтобы я мог написать
using boost::adaptors::reversed_if;
for (auto const & x : range | reversed_if(reverse)) do_stuff(x);
Я могу реализовать это сам (начиная с Вот) но я не уверен, как поступить. Боюсь, что для поддержки условий выполнения мне придется проверять логическое значение на каждой итерации, чтобы определить направление итерации, или использовать виртуальность для отправки кода итерации. Это причина, почему это не предусмотрено в адаптерах серии Boost?
Любое альтернативное решение?
Если вы хотите избежать проверки во время выполнения каждого приращения, в какую сторону идти, вы должны преобразовать значение времени выполнения в значение времени компиляции вне структуры цикла.
В этом случае мы хотим, чтобы диапазон, в котором мы зацикливались, изменялся, а тело — нет.
легко способ сделать это состоит в том, чтобы написать лямбду для тела, затем иметь переключатель, чтобы выбрать, какой цикл выбрать.
auto do_stuff = [&](auto&& elem){ /* code */ };
if (reverse) {
using boost::adaptors::reversed;
for (auto const & x : range | reversed) do_stuff(x);
} else {
for (auto const & x : range) do_stuff(x);
}
мы выполнили диспетчеризацию вне цикла, создав два разных цикла со статической информацией о типе цикла.
Мы можем сделать адаптер следующим образом:
magic_switch
( reverse )
( range, range|reversed )
(
[&](auto&& range){
for (auto const& x : decltype(range)(range)) {
do_stuff(x);
}
}
);
где magic_switch
берет индекс (std::size_t
) в качестве первого аргумента. Возвращает лямбду, которая принимает список аргументов. Он возвращает лямбду, которая принимает лямбду и передает ей аргумент из 2-го списка, как определено индексом первого аргумента, в этот список.
inline auto magic_switch( std::size_t I ) {
return [I](auto&&...options) {
return [I, &](auto&& f)->decltype(auto) {
using fptr = void(*)(void const volatile* op, decltype(f));
static const fptr table[] = {
+[](void const volatile* op_in, decltype(f) f) {
auto* option = static_cast<std::decay_t<decltype(options)>*>(op_in);
decltype(f)(f)( decltype(options)(*option) );
}...
};
const volatile void* ptrs[] = {std::addressof(options)...};
if (I >= sizeof...(options)) I = sizeof...(options)-1;
if (I == -1) return;
table[I]( ptrs[I], decltype(f)(f) );
};
};
}
это эскиз при реализации (он почти наверняка содержит ошибки сборки).
Сложность в том, что «поток типов» (для обозначения термина) не идет так, как вы обычно этого хотите. Поэтому я вынужден в основном использовать стиль продолжения.
Обратите внимание, что многие компиляторы недовольны расширением пакета, содержащим целую лямбду. Вспомогательная функция, которая возвращает указатель на функцию, может быть написана:
template<class F>
using f_ptr = void(*)(const volatile void*, F&&);
template<class Option, class F>
f_ptr<F> get_f_ptr() {
return +[](void const volatile* op_in, F&& f) {
auto* option = static_cast<std::decay_t<Option>*>(op_in);
std::forward<F>(f)( std::forward<Option>(*option) );
};
}
затем замените таблицу на:
static const fptr table[] = {
get_fptr<decltype(options), decltype(f)>()...
};
на этих компиляторах.
Других решений пока нет …