在 运行 时间选择模板参数时如何避免代码呈指数增长
How to avoid exponential increase in code when choosing template arguments at run-time
考虑一堆基本类型,Foo
,所有类型都具有通用方法的独特实现,Bar()
。我可以像这样组合 Foo1
、Foo2
、Foo5
:
CombinedFoo<Foo1, Foo2, Foo5> combined_foo;
它使用递归继承使 CombinedFoo
实际上等同于:
class CombinedFoo <Foo1, Foo2, Foo5>
{
Foo1 foo1;
Foo2 foo2;
Foo5 foo5;
public:
void Bar ()
{
foo1.Bar();
foo2.Bar();
foo5.Bar();
}
};
这很方便,但是我 运行 遇到了一个问题,当我想在 运行 时选择将哪些 Foo
类型组合(成一个对象)发送到函数,说:
template <typename Foo> void Do (Foo && foo);
使用 if
s 和 switch
s 解决 3 选项版本的示例解决方案:
int result = 0;
if (criteria_for_foo1)
result += 100;
if (criteria_for_foo2)
result += 10;
if (criteria_for_foo3)
result += 1;
switch (result)
{
case 001 : Do(Foo3());
break;
case 010 : Do(Foo2());
break;
case 011 : Do(CombinedFoo<Foo2, Foo3>());
break;
case 100 : Do(Foo1());
break;
case 101 : Do(CombinedFoo<Foo1, Foo3>());
break;
case 110 : Do(CombinedFoo<Foo1, Foo2>());
break;
case 111 : Do(CombinedFoo<Foo1, Foo2, Foo3>());
break;
default : break;
}
if
语句很好,它们呈线性增长,但是 switch
语句随着我们有更多选择而呈指数增长。我的现实世界问题有 4 个选项,所以我需要处理 16 个我不想维护的案例。
我相信没有办法避免可执行文件呈指数增长,但是有没有办法在 c++ 代码中避免这种情况(而不在 Bar
方法中引入明显的低效率)?或者对于这个一般问题是否有已知的解决方法/替代方法?
编辑:
为清楚起见:Do(Foo1); Do(Foo2)
与 Do(CombinedFoo<Foo1, Foo2>())
不同,重要的是 Foo
组合在一起用于对 Do
的单个调用。
对于那些想了解真实世界动机的人:这是一个优化问题,其中我的 Foo
实际上是 Generator
的基础 Move
可以编辑我的解决方案,然后将其发送到各种启发式方法中。如果我一次只发送一个 Generator
那么我的求解器将重复相同类型的移动数千次,因此总是没有效率/停留在局部最小值(反复考虑相同类型的移动是众所周知具有这种效果)。
我在 运行 时 select 一些模板参数的原因是因为一些 Move
不适合某些问题实例(我的程序不适合直到 运行-time 才意识到。
不确定这是您需要的,但是这个怎么样:
Foo *obj1 = nullptr;
Foo *obj2 = nullptr;
Foo *obj3 = nullptr;
Foo *obj4 = nullptr;
if (cond1) { obj1 = new Foo1; /* do more stuff here */ }
else if (cond2) { obj2 = new Foo5; /* do more stuff here */ }
else if (cond3) { obj4 = new Foo5; /* do more stuff here */ }
else { obj3 = new Foo3; /* do more stuff here */ }
然后,调用这个函数:
void func(
Foo *obj1,
Foo *obj2,
Foo *obj3,
Foo *obj4)
{
if (obj1) obj1->bar();
if (obj2) obj2->bar();
if (obj3) obj3->bar();
if (obj4) obj4->bar();
}
这是一个可能的解决方案:
struct DisabledFoo
{
void Bar() {}
};
template <
size_t Combination,
typename Disabled,
typename... Foos,
size_t... Is
>
auto FoosFor(std::index_sequence<Is...>)
{
return CombinedFoo<
std::conditional_t<
static_cast<bool>(Combination & 1 << (sizeof...(Foos) - 1 - Is)),
Foos,
Disabled
>...
>{};
}
template <
typename F,
size_t Combination,
typename Disabled,
typename... Foos
>
void FooDo(const F& func) {
func(
FoosFor<
Combination,
Disabled,
Foos...
>(std::make_index_sequence<sizeof...(Foos)>())
);
}
template <
typename F,
typename Disabled,
typename... Foos,
size_t... Combinations
>
constexpr auto MakeFooDoers(std::index_sequence<Combinations...>) {
return std::array{ FooDo<F, Combinations, Disabled, Foos...>... };
}
constexpr size_t constexpr_pow(size_t base, size_t exp)
{
size_t ret = 1;
for (size_t i = 0; i < exp; ++i) {
ret *= base;
}
return ret;
}
template <
typename F,
typename Disabled,
typename... Foos
>
constexpr std::array FooDoers{
MakeFooDoers<
F,
Disabled,
Foos...
>(std::make_index_sequence<constexpr_pow(2, sizeof...(Foos))>())
};
template <typename F>
constexpr void DoCombination(size_t combination, const F& func) {
FooDoers<
F,
DisabledFoo,
Foo1,
Foo2,
Foo3
>[combination](func);
}
称之为
DoCombination(0b111, [](auto foo) { Do(foo); });
这尽可能避免了运行时开销,只进行一次间接调用。它通过构建一个 constexpr
函数指针数组来工作,每个函数指针分派到不同的 CombinedFoo
,其中虚拟实现 DisabledFoo
用于我们实际上不想调用的 Foos .
基本上,FooDoers
最终看起来像这样:
constexpr std::array FooDoers = {
[] { Do(CombinedFoo<DisabledFoo, DisabledFoo, DisabledFoo>{}); },
[] { Do(CombinedFoo<DisabledFoo, DisabledFoo, Foo3>{}); },
[] { Do(CombinedFoo<DisabledFoo, Foo2, DisabledFoo>{}); },
[] { Do(CombinedFoo<DisabledFoo, Foo2, Foo3>{}); },
[] { Do(CombinedFoo<Foo1, DisabledFoo, DisabledFoo>{}); },
[] { Do(CombinedFoo<Foo1, DisabledFoo, Foo3>{}); },
[] { Do(CombinedFoo<Foo1, Foo2, DisabledFoo>{}); },
[] { Do(CombinedFoo<Foo1, Foo2, Foo3>{}); }
};
然后我们对其进行索引并调用正确的包装器。
要添加另一个 Foo
class,只需将其作为另一个参数添加到 DoCombination
中的 FooDoers
。
从 run-time 的任何类型列表中选择类型的通用方法:
界面如下:
template <typename ... Types, typename Functor>
void ChooseTypes (const bool chosen[], Functor && f)
其中 chosen[0] = true
表示从列表中选择第一种类型。
在内部,类型像这样转发给仿函数:f()<ChosenTypes...>()
保持顺序。
使用方法:
struct MyFunctor
{
// data/references can be stored/passed here
template <typename ... Ts>
void operator () ()
{
// Ts = the chosen types
// Here's where we get to use them...
}
};
// later in func/method:
bool chosen[5];
// choose which types you want
MyFunctor f;
// pass data in the functor
// (const/& by constructor)
// Then this will call the functor with the chosen types:
ChooseTypes<T1, T2, T3, T4, T5>(chosen, f);
图书馆代码:
namespace ChooseTypesRecursive // internal use only
{
// CTR = ChooseTypesRecursive
template <int N, typename Functor>
struct CTR
{
using Next = CTR<N-1, Functor>;
template <typename CandidateType, typename ... Args>
static void Ctr (const bool * chosen, Functor & f)
{
if (*chosen)
{
Next::template Ctr<Args..., CandidateType>
(++chosen, f);
}
else
{
Next::template Ctr<Args...>
(++chosen, f);
}
}
};
template <typename Functor>
struct CTR <0, Functor>
{
template <typename ... ChosenTypes>
static void Ctr (const bool *, Functor & f)
{
f.template operator()<ChosenTypes...>();
}
};
} // namespace ChooseTypesRecursive
template <typename ... Types, typename Functor>
void ChooseTypes (const bool chosen[], Functor && f)
{
constexpr int N = sizeof...(Types);
using namespace ChooseTypesRecursive;
CTR<N, Functor>::template Ctr<Types...>(chosen, f);
}
解决具体问题:
考虑一堆基本类型,Foo
,所有类型都具有通用方法的独特实现,Bar()
。我可以像这样组合 Foo1
、Foo2
、Foo5
:
CombinedFoo<Foo1, Foo2, Foo5> combined_foo;
它使用递归继承使 CombinedFoo
实际上等同于:
class CombinedFoo <Foo1, Foo2, Foo5>
{
Foo1 foo1;
Foo2 foo2;
Foo5 foo5;
public:
void Bar ()
{
foo1.Bar();
foo2.Bar();
foo5.Bar();
}
};
这很方便,但是我 运行 遇到了一个问题,当我想在 运行 时选择将哪些 Foo
类型组合(成一个对象)发送到函数,说:
template <typename Foo> void Do (Foo && foo);
使用 if
s 和 switch
s 解决 3 选项版本的示例解决方案:
int result = 0;
if (criteria_for_foo1)
result += 100;
if (criteria_for_foo2)
result += 10;
if (criteria_for_foo3)
result += 1;
switch (result)
{
case 001 : Do(Foo3());
break;
case 010 : Do(Foo2());
break;
case 011 : Do(CombinedFoo<Foo2, Foo3>());
break;
case 100 : Do(Foo1());
break;
case 101 : Do(CombinedFoo<Foo1, Foo3>());
break;
case 110 : Do(CombinedFoo<Foo1, Foo2>());
break;
case 111 : Do(CombinedFoo<Foo1, Foo2, Foo3>());
break;
default : break;
}
if
语句很好,它们呈线性增长,但是 switch
语句随着我们有更多选择而呈指数增长。我的现实世界问题有 4 个选项,所以我需要处理 16 个我不想维护的案例。
我相信没有办法避免可执行文件呈指数增长,但是有没有办法在 c++ 代码中避免这种情况(而不在 Bar
方法中引入明显的低效率)?或者对于这个一般问题是否有已知的解决方法/替代方法?
编辑:
为清楚起见:Do(Foo1); Do(Foo2)
与 Do(CombinedFoo<Foo1, Foo2>())
不同,重要的是 Foo
组合在一起用于对 Do
的单个调用。
对于那些想了解真实世界动机的人:这是一个优化问题,其中我的 Foo
实际上是 Generator
的基础 Move
可以编辑我的解决方案,然后将其发送到各种启发式方法中。如果我一次只发送一个 Generator
那么我的求解器将重复相同类型的移动数千次,因此总是没有效率/停留在局部最小值(反复考虑相同类型的移动是众所周知具有这种效果)。
我在 运行 时 select 一些模板参数的原因是因为一些 Move
不适合某些问题实例(我的程序不适合直到 运行-time 才意识到。
不确定这是您需要的,但是这个怎么样:
Foo *obj1 = nullptr;
Foo *obj2 = nullptr;
Foo *obj3 = nullptr;
Foo *obj4 = nullptr;
if (cond1) { obj1 = new Foo1; /* do more stuff here */ }
else if (cond2) { obj2 = new Foo5; /* do more stuff here */ }
else if (cond3) { obj4 = new Foo5; /* do more stuff here */ }
else { obj3 = new Foo3; /* do more stuff here */ }
然后,调用这个函数:
void func(
Foo *obj1,
Foo *obj2,
Foo *obj3,
Foo *obj4)
{
if (obj1) obj1->bar();
if (obj2) obj2->bar();
if (obj3) obj3->bar();
if (obj4) obj4->bar();
}
这是一个可能的解决方案:
struct DisabledFoo
{
void Bar() {}
};
template <
size_t Combination,
typename Disabled,
typename... Foos,
size_t... Is
>
auto FoosFor(std::index_sequence<Is...>)
{
return CombinedFoo<
std::conditional_t<
static_cast<bool>(Combination & 1 << (sizeof...(Foos) - 1 - Is)),
Foos,
Disabled
>...
>{};
}
template <
typename F,
size_t Combination,
typename Disabled,
typename... Foos
>
void FooDo(const F& func) {
func(
FoosFor<
Combination,
Disabled,
Foos...
>(std::make_index_sequence<sizeof...(Foos)>())
);
}
template <
typename F,
typename Disabled,
typename... Foos,
size_t... Combinations
>
constexpr auto MakeFooDoers(std::index_sequence<Combinations...>) {
return std::array{ FooDo<F, Combinations, Disabled, Foos...>... };
}
constexpr size_t constexpr_pow(size_t base, size_t exp)
{
size_t ret = 1;
for (size_t i = 0; i < exp; ++i) {
ret *= base;
}
return ret;
}
template <
typename F,
typename Disabled,
typename... Foos
>
constexpr std::array FooDoers{
MakeFooDoers<
F,
Disabled,
Foos...
>(std::make_index_sequence<constexpr_pow(2, sizeof...(Foos))>())
};
template <typename F>
constexpr void DoCombination(size_t combination, const F& func) {
FooDoers<
F,
DisabledFoo,
Foo1,
Foo2,
Foo3
>[combination](func);
}
称之为
DoCombination(0b111, [](auto foo) { Do(foo); });
这尽可能避免了运行时开销,只进行一次间接调用。它通过构建一个 constexpr
函数指针数组来工作,每个函数指针分派到不同的 CombinedFoo
,其中虚拟实现 DisabledFoo
用于我们实际上不想调用的 Foos .
基本上,FooDoers
最终看起来像这样:
constexpr std::array FooDoers = {
[] { Do(CombinedFoo<DisabledFoo, DisabledFoo, DisabledFoo>{}); },
[] { Do(CombinedFoo<DisabledFoo, DisabledFoo, Foo3>{}); },
[] { Do(CombinedFoo<DisabledFoo, Foo2, DisabledFoo>{}); },
[] { Do(CombinedFoo<DisabledFoo, Foo2, Foo3>{}); },
[] { Do(CombinedFoo<Foo1, DisabledFoo, DisabledFoo>{}); },
[] { Do(CombinedFoo<Foo1, DisabledFoo, Foo3>{}); },
[] { Do(CombinedFoo<Foo1, Foo2, DisabledFoo>{}); },
[] { Do(CombinedFoo<Foo1, Foo2, Foo3>{}); }
};
然后我们对其进行索引并调用正确的包装器。
要添加另一个 Foo
class,只需将其作为另一个参数添加到 DoCombination
中的 FooDoers
。
从 run-time 的任何类型列表中选择类型的通用方法:
界面如下:
template <typename ... Types, typename Functor>
void ChooseTypes (const bool chosen[], Functor && f)
其中 chosen[0] = true
表示从列表中选择第一种类型。
在内部,类型像这样转发给仿函数:f()<ChosenTypes...>()
保持顺序。
使用方法:
struct MyFunctor
{
// data/references can be stored/passed here
template <typename ... Ts>
void operator () ()
{
// Ts = the chosen types
// Here's where we get to use them...
}
};
// later in func/method:
bool chosen[5];
// choose which types you want
MyFunctor f;
// pass data in the functor
// (const/& by constructor)
// Then this will call the functor with the chosen types:
ChooseTypes<T1, T2, T3, T4, T5>(chosen, f);
图书馆代码:
namespace ChooseTypesRecursive // internal use only
{
// CTR = ChooseTypesRecursive
template <int N, typename Functor>
struct CTR
{
using Next = CTR<N-1, Functor>;
template <typename CandidateType, typename ... Args>
static void Ctr (const bool * chosen, Functor & f)
{
if (*chosen)
{
Next::template Ctr<Args..., CandidateType>
(++chosen, f);
}
else
{
Next::template Ctr<Args...>
(++chosen, f);
}
}
};
template <typename Functor>
struct CTR <0, Functor>
{
template <typename ... ChosenTypes>
static void Ctr (const bool *, Functor & f)
{
f.template operator()<ChosenTypes...>();
}
};
} // namespace ChooseTypesRecursive
template <typename ... Types, typename Functor>
void ChooseTypes (const bool chosen[], Functor && f)
{
constexpr int N = sizeof...(Types);
using namespace ChooseTypesRecursive;
CTR<N, Functor>::template Ctr<Types...>(chosen, f);
}