【问题标题】:C++ convert variables to template argumentsC++ 将变量转换为模板参数
【发布时间】:2013-10-13 20:06:42
【问题描述】:

我想使用模板进行优化,如here 所述。但是,随着 bool 模板参数数量的增加,实例化模板可能有太多的分支。如果你使用更大的枚举而不是布尔值,它会变得更加复杂。

#include <iostream>
using namespace std;

template <bool b1, bool b2>
int HeavyLoop_impl(int arg)
{
    for (int i = 0; i < 10000000; i++)
    {
        // b1 is known at compile-time, so this branch will be eliminated
        if (b1) { arg += 1; }
        else    { arg += 2; }

        // b2 is known at compile-time, so this branch will be eliminated
        if (b2) { arg += 10; }
        else    { arg += 20; }
    }
    return arg;
}

// This function could be generated automatically
void HeavyLoop(bool b1, bool b2, int arg)
{
    int res;
    if (b1) {
        if (b2) { res = HeavyLoop_impl<true, true>(arg); }
        else    { res = HeavyLoop_impl<true, false>(arg); }
    } else {
        if (b2) { res = HeavyLoop_impl<false, true>(arg); }
        else    { res = HeavyLoop_impl<false, false>(arg); }
    }
    cout << "res: "<<res<<endl;
}

int main(int argc, char**argv)
{
    bool b1 = true;
    bool b2 = false;
    int arg = 0;
    HeavyLoop(b1, b2, arg);
    return 0;
}

有没有办法自动生成HeavyLoop函数?我想要这样的东西:

vars_to_template_function<bool, bool>(HeavyLoop_impl, b1, b2, arg);

这有可能吗?感谢您的任何提示。

注意:这只是一个非常简化的例子。实际的循环当然更复杂:o)

【问题讨论】:

  • 您的问题中有几个令人不安的地方。首先 b1 和 b2 在 main 中有硬编码值,所以即使不是模板,代码也会被优化掉,并且会直接调用右分支(假设你有一个下降编译器)。相反,如果 main 中的 b1 和 b2 是动态值,那么使用模板函数绝对没有优势,因为在 HeavyLoop 中无论如何都会进行分支......
  • 这与链接并不真正匹配,因为它具有完全不同的代码,具体取决于标志。
  • @log0 除了在HeavyLoop 中,分支是由编译时已知的模板参数完成的,因此实际上不会发生分支。这就是问题的重点 - 将运行时知识(两个布尔值)转换为编译时(从 4 个具有硬编码分支的函数之一中选择)。
  • 用Boost.preprocessor应该可以生成这样的“驱动”函数,但是我现在没时间自己尝试,抱歉。
  • HeavyLoop_impl 是否如此繁重以至于编译器拒绝将其内联到 main 中?您实际上不必执行任何这些操作,优化器将知道 b1 和 b2 的值 对于该调用 一旦调用被内联,因为参数的值在编译时间(这是一个微不足道的数据流分析,表明它们在初始化后没有被修改,并且初始化来自编译时间常数)。然后它可以从内联到main的代码中消除死代码。

标签: c++ templates c++11


【解决方案1】:

我决定让代码更有趣,这是我第一次尝试的改进版本,具有以下好处:

  • 支持enum 类型
  • 明确指定应该转换多少个参数
  • 复杂部分的通用实现,每个使用它的函数都有一个小助手。

代码:

#include <iostream>
#include <utility>
#include <type_traits>

// an enum we would like to support
enum class tribool { FALSE, TRUE, FILE_NOT_FOUND };

// declare basic generic template
// (independent of a specific function you'd like to call)
template< template< class > class CB, std::size_t N, typename = std::tuple<> >
struct var_to_template;

// register types that should be supported
template< template< class > class CB, std::size_t N, typename... Cs >
struct var_to_template< CB, N, std::tuple< Cs... > >
{
    // bool is pretty simple, there are only two values
    template< typename R, typename... Args >
    static R impl( bool b, Args&&... args )
    {
        return b
          ? var_to_template< CB, N-1, std::tuple< Cs..., std::true_type > >::template impl< R >( std::forward< Args >( args )... )
          : var_to_template< CB, N-1, std::tuple< Cs..., std::false_type > >::template impl< R >( std::forward< Args >( args )... );
    }

    // for each enum, you need to register all its values
    template< typename R, typename... Args >
    static R impl( tribool tb, Args&&... args )
    {
        switch( tb ) {
        case tribool::FALSE:
          return var_to_template< CB, N-1, std::tuple< Cs..., std::integral_constant< tribool, tribool::FALSE > > >::template impl< R >( std::forward< Args >( args )... );
        case tribool::TRUE:
          return var_to_template< CB, N-1, std::tuple< Cs..., std::integral_constant< tribool, tribool::TRUE > > >::template impl< R >( std::forward< Args >( args )... );
        case tribool::FILE_NOT_FOUND:
          return var_to_template< CB, N-1, std::tuple< Cs..., std::integral_constant< tribool, tribool::FILE_NOT_FOUND > > >::template impl< R >( std::forward< Args >( args )... );
        }
        throw "unreachable";
    }

    // in theory you could also add int, long, ... but
    // you'd have to switch on every possible value that you want to support!
};

// terminate the recursion
template< template< class > class CB, typename... Cs >
struct var_to_template< CB, 0, std::tuple< Cs... > >
{
    template< typename R, typename... Args >
    static R impl( Args&&... args )
    {
        return CB< std::tuple< Cs... > >::template impl< R >( std::forward< Args >( args )... );
    }
};

// here's your function with the template parameters
template< bool B, tribool TB >
int HeavyLoop_impl( int arg )
{
    for( int i = 0; i < 10000000; i++ ) {
        arg += B ? 1 : 2;
        arg += ( TB == tribool::TRUE ) ? 10 : ( TB == tribool::FALSE ) ? 20 : 30;
    }
    return arg;
}

// a helper class, required once per function that you'd like to forward
template< typename > struct HeavyLoop_callback;
template< typename... Cs >
struct HeavyLoop_callback< std::tuple< Cs... > >
{
    template< typename R, typename... Args >
    static R impl( Args&&... args )
    {
        return HeavyLoop_impl< Cs::value... >( std::forward< Args >( args )... );
    }
};

// and here, everything comes together:
int HeavyLoop( bool b, tribool tb, int arg )
{
    // you provide the helper and the number of arguments
    // that should be converted to var_to_template<>
    // and you provide the return type to impl<>
    return var_to_template< HeavyLoop_callback, 2 >::impl< int >( b, tb, arg );
}

int main()
{
    bool b = true;
    tribool tb = tribool::FALSE;
    int arg = 0;
    int res = HeavyLoop( b, tb, arg );
    std::cout << "res: " << res << std::endl;
    return 0;
}

这里有一个live example,以备不时之需。

【讨论】:

  • @MichalFapso 我用一个大大改进的版本更新了我的答案,希望你喜欢。 :)
  • 这个结构很巧妙。但是我将如何修改它以扩展类型模板参数?目前,它只适用于非类型模板参数。
  • Cs... 扩展为模板参数列表,然后将另一种类型 std::true_type 作为参数传递。这些是形成 std​​::tuple 的类型。
  • 默认在HeavyLoop 调用var_to_template 时使用只有两个参数,其他使用指定具有更多参数的元组。并且专业化独立于默认值。
  • 您缺少一个中间步骤。模板声明的列表必须与模板类名之后的特化列表匹配。特化的模板参数与模板类声明的参数无关,只是它们必须从特化的类型推导出来。所以:template&lt; LIST1 &gt; struct A { ... }; 和 template&lt; LIST2 &gt; struct A&lt; MATCH1 &gt; { ... }; - 这里 MATCH1 必须被 LIST1 接受,而 LIST2 是从 MATCH1 推导出来的。
【解决方案2】:

你可以这样做:

#include <iostream>
using namespace std;

template <bool b1, bool b2>
struct HeavyLoopImpl
{
    static int func(int arg)
    {
        for (int i = 0; i < 10000000; i++) {
            arg += b1 ? 1 : 2;
            arg += b2 ? 10 : 20;
        }
        return arg;
    }
};

template <template<bool...> class Impl,bool...Bs>
struct GenericJump
{
    template<typename... Args>
    static int impl(Args&&... args)
    {
        return Impl<Bs...>::func(std::forward<Args>(args)...);
    }

    template<typename... Args>
    static int impl(bool b, Args&&... args)
    {
        return b
            ? GenericJump<Impl,Bs...,true >::impl(std::forward<Args>(args)...)
            : GenericJump<Impl,Bs...,false>::impl(std::forward<Args>(args)...);
    }
};

int HeavyLoop(bool b1, bool b2, int arg)
{
    return GenericJump<HeavyLoopImpl>::impl(b1,b2,arg);
}

int main()
{
    bool b1 = true;
    bool b2 = false;
    int arg = 0;
    int res = HeavyLoop(b1, b2, arg);
    cout << "res: "<<res<<endl;
    return 0;
}

这基本上是 Daniels 的解决方案,但它允许您使用 HeavyLoop_impl() 以外的函数作为实现。仅能够调用单个模板函数就违背了成为通用解决方案的目的。 GenericJump 模板类也可以调用其他函数。您只需要将HeavyLoop_impl() 模板函数更改为带有静态函数func() 的模板类。它非常有效。它使用 gcc 4.7.3 编译并给出正确的输出。

【讨论】:

  • 非常感谢,丹尼尔和拉尔夫!现在,如果 HeavyLoop 是某个类 A 的成员方法,使用它的成员变量,那么使用 HeavyLoopImpl 仿函数的最佳方式是什么?我可以将它声明为 A 的朋友,但我还必须稍微更改仿函数的代码,为所有成员变量添加前缀(a.memberInt 而不是 memberInt)。并且将模板函子声明为朋友需要先声明它,然后再将其声明为朋友。有更清洁的解决方案吗?
  • 我相信这里没有足够的关于设计问题的信息。不同的方法有不同的优点和缺点。但请考虑以下事项:将HeavyLoopImpl 作为嵌套结构私下隐藏在您的A 类中。如果代码变得臃肿,您甚至可以在A 中前向声明它。添加前缀a. 对我来说听起来并不那么糟糕。如果你真的想避免在A 中声明一个成员模板函数并在那里复制HeavyLoopImpl 的功能。然后从你的嵌套仿函数结构中调用这个函数。
【解决方案3】:

理想的通用解决方案实际上取决于您想要改变什么。变化的可能性是:

  • 分支的数量,以及被分支的变量的类型。
  • 要执行的操作及其参数的数量和类型。

除非您确实需要更改所有这些内容,否则我建议不要制作完全通用的解决方案。只考虑你想改变的事情,你的生活会更轻松。

假设总分支数小于2^64,可以使用switch语句进行dispatch。以下解决方案演示了它是如何工作的:

template<unsigned permutation>
struct Permutation
{
    static_assert(permutation < 4, "permutation must be in the range [0, 4)");
    static const bool b1 = permutation & (1 << 0);
    static const bool b2 = permutation & (1 << 1);
};

unsigned makePermutation(bool b1, bool b2)
{
    return (b1 << 0) | (b2 << 1);
}

template<unsigned p>
int HeavyLoop_impl(int arg)
{
    return HeavyLoop_impl<Permutation<p>::b1, Permutation<p>::b2>(arg);
}

int HeavyLoop_impl(unsigned permutation, int arg)
{
    switch(permutation)
    {
    case 0: return HeavyLoop_impl<0>(arg);
    case 1: return HeavyLoop_impl<1>(arg);
    case 2: return HeavyLoop_impl<2>(arg);
    case 3: return HeavyLoop_impl<3>(arg);
    }
}

[注意:使用 Boost.Preprocessor 生成上述 switch 语句将是微不足道的。]

void HeavyLoop(bool b1, bool b2, int arg)
{
    int res = HeavyLoop_impl(makePermutation(b1, b2), arg);
    cout << "res: "<<res<<endl;
}

【讨论】:

    【解决方案4】:

    我认为您问题的最佳答案实际上不是自动生成它,而是按照您在问题中已有的方式保留它。

    制作一个自动模板函数来生成中间立场只会混淆你首先要做的不变切换。

    比起人们为你提供的任何答案,我更愿意尝试理解中间层在你的问题中是如何工作的。

    我有一个类似的例子。就我而言,我可以在一组值之间应用许多不同的操作。数组大小相等。但是,我也有一个结构,可以将数组的子范围映射到影响我的操作的权重值。 因此,例如,我可能正在使用 100 个值的数组,并且具有这样的权重范围:

    [0,25] rangeWeight = 0
    [26,35] rangeWeight = 0.25
    [36,50] rangeWeight = 0.5
    [51,99] rangeWeight = 1.0
    

    所以每个操作看起来像(伪):

    for each subrange:
        alias to the dst buffer
        alias to the src buffer
        determine the number of elements in the range
        if there's any
            weight = weightPassedIn * rangeWeight;
    
            Op(dst, src, weight, numElements);
    

    对我来说,有几个优化涉及是否触摸目的地(如果它仍然处于清除值,可以做出一些假设来简化每个操作的数学运算),另外,如果重量恰好是完整,1.0,还有其他快捷键。

    起初,我使用相同的设置一遍又一遍地编写相同的循环,一旦我将每个操作周围的所有循环重构为函数,我就很自然地拥有了你的不变量包装器的形式。实际上有几个包装器主要只是包装循环内发生的高级操作,否则只处理这样的个别优化:

    if (weight == 1.0f)
    {
        if ( arrayIsCleared )
            Blend<BlendOpSet, true, false>(otherBuff, subRangesMask, 1.0f);
        else
            Blend<BlendOpAccumulate, true, false>(otherBuff, subRangesMask, 1.0f);
    }
    else
    {
        if ( arrayIsCleared )
            Blend<BlendOpSet, false, false>(otherBuff, subRangesMask, weight);
        else
            Blend<BlendOpAccumulate, false, false>(otherBuff, subRangesMask, weight);
    }
    

    【讨论】:

    • 当然,你是对的,当你只有 2 个布尔参数时,自己进行分支可能更具可读性。但是如果有更多的变量和一个有 10 个可能值的枚举,它就会变得过于“分支”并且可读性大大降低。
    • @MichalFapso 嗯,在这个例子中,包装器是一个“累积”操作,有很多操作,它们都有相似的包装器,但至少逻辑容易。
    【解决方案5】:

    您是否考虑过将函数作为模板参数传递?这样您就可以根据自己的意愿定制内部循环。

    Function passed as template argument

    不过,函数调用会有一点开销。

    【讨论】:

    • 一个函数不能被内联,所以会有开销。但是 functor 可以并且不会有任何开销(前提是 operator() 被正确声明为内联并且它的定义可见)。
    • @JanHudec 不正确。可以内联通过具有在编译时已知的地址的指向函数的调用。
    • @willj:是的,它可以提供指针是已知的等等,尤其是编译器会打扰。函子在实践中更有可能被内联。
    【解决方案6】:

    这是另一个使用 boost::hana 的解决方案,它也可以处理枚举:

    #include <cstdio>
    #include <type_traits>
    #include <boost/hana.hpp>
    
    namespace hana = boost::hana;
    
    template <typename F, typename TArgs, typename TIn, typename TOut>
    void fun_arg_combinations_impl(F&& f, TArgs targs, TIn tin, TOut tout) {
        if constexpr (hana::is_empty(tin)) {
            hana::unpack(tout, f);
        } else {
            hana::for_each(hana::front(tin), [&](auto v){
                if (v == hana::front(targs)) {
                    fun_arg_combinations_impl(f, hana::drop_front(targs), hana::drop_front(tin), hana::append(tout, v));
                }
            });
        }
    }
    
    template <typename F, typename TArgs, typename TIn>
    void fun_arg_combinations(F&& f, TArgs targs, TIn tin) {
        fun_arg_combinations_impl(f, targs, tin, hana::tuple<>());
    }
    
    enum Shape {LINE, CIRCLE, SQUARE};
    
    int main()
    {
        auto f_heavy_loop = [](auto b1t, auto b2t, auto st) {
            constexpr bool b1 = decltype(b1t)::value;
            constexpr bool b2 = decltype(b2t)::value;
            constexpr Shape s = decltype(st )::value;
    
            printf("out:%d %d %d\n", b1, b2, s);
        };
    
        //constexpr auto bools = hana::make_tuple(std::true_type{}, std::false_type{});
        constexpr auto bools = hana::tuple<std::true_type, std::false_type>{};
        constexpr auto shapes = hana::tuple<
            std::integral_constant<Shape, LINE>,
            std::integral_constant<Shape, CIRCLE>,
            std::integral_constant<Shape, SQUARE>>{};
    
        // Using volatile to not allow the compiler to optimize for hard-coded values
        volatile bool b1 = true;
        volatile bool b2 = false;
        volatile Shape s = SQUARE;
        fun_arg_combinations(
            f_heavy_loop,
            hana::make_tuple(b1   , b2   , s     ),
            hana::make_tuple(bools, bools, shapes));
    }
    

    f_heavy_loop() lambda 中的b1、b2 和s 都是constexpr,所以我们可以对它们使用if constexpr。

    输出:

    out:1 0 2
    

    在此处查看生成的程序集:https://godbolt.org/z/nsF2l5

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-11-01
      • 2017-08-07
      • 1970-01-01
      • 1970-01-01
      • 2014-06-20
      • 2016-11-26
      • 1970-01-01
      • 2014-04-30
      相关资源
      最近更新 更多