【问题标题】:How else to achieve "templated function pointers"?还有什么方法可以实现“模板化函数指针”?
【发布时间】:2009-02-18 09:05:47
【问题描述】:

是否可以建立一组模板化的函数指针,而无需手动操作?这是一个例子来说明我在说什么。

假设我有一个经常被调用的函数“write”,我有两个实现(write0 和 write1),我希望能够在它们之间动态切换。这些写函数是在参数类型上模板化的。一种方法是只使用一个模板化的前端函数 write(),它在内部使用 if 语句。

事实证明这对我的需要来说已经足够快了,但现在我想知道我是否可以使用函数指针来做同样的事情(只是为了好玩)。这种方法的问题是设置函数指针很麻烦。有没有其他方法可以基本上实现 write() 的理想但没有条件(直接静态调度)?

(其他“规则”:我无法将 Msg 类更改为具有 write() 方法,也无法更改使用站点代码以将 Msgs 替换为 Msgs 的适配器。)

FWIW,我发现 this article 基本上和我在这里说的一样。

#include <iostream>
using namespace std;

template<typename T> void write0(T msg) { cout << "write0: " << msg.name() << endl; }
template<typename T> void write1(T msg) { cout << "write1: " << msg.name() << endl; }

// This isn't so bad, since it's just a conditional (which the processor will
// likely predict correctly most of the time).
bool use_write0;
template<typename T> void write(T msg) { if (use_write0) write0(msg); else write1(msg); }

struct MsgA { const char *name() { return "MsgA"; } };
struct MsgB { const char *name() { return "MsgB"; } };
struct MsgC { const char *name() { return "MsgC"; } };
struct MsgD { const char *name() { return "MsgD"; } };

// This doesn't work: templates may not be virtual.
#if 0
struct Writer { template<typename T> virtual void write(T msg) = 0; };
struct Writer0 { template<typename T> virtual void write(T msg) { cout << "write0: " << msg.name() << endl; } };
struct Writer1 { template<typename T> virtual void write(T msg) { cout << "write0: " << msg.name() << endl; } };
#endif

int main(int argc, char **argv) {
  use_write0 = argc == 1;

  // I can do this:
  write(MsgA());

  // Can I achieve the following without the verbosity (manual setup, named
  // template instantiations, etc.)?
  void (*pwriteA)(MsgA) = use_write0 ? (void(*)(MsgA)) write0<MsgA> : (void(*)(MsgA)) write1<MsgA>;
  void (*pwriteB)(MsgB) = use_write0 ? (void(*)(MsgB)) write0<MsgB> : (void(*)(MsgB)) write1<MsgB>;
  void (*pwriteC)(MsgC) = use_write0 ? (void(*)(MsgC)) write0<MsgC> : (void(*)(MsgC)) write1<MsgC>;
  void (*pwriteD)(MsgD) = use_write0 ? (void(*)(MsgD)) write0<MsgD> : (void(*)(MsgD)) write1<MsgD>;
  pwriteA(MsgA());
  pwriteB(MsgB());
  pwriteC(MsgC());
  pwriteD(MsgD());

  return 0;
}

【问题讨论】:

    标签: c++ templates function-pointers


    【解决方案1】:

    如果你想在程序运行时来回切换日志功能,我认为你必须手动设置每种类型的函数指针。

    如果只在启动时选择日志记录函数就足够了,那么它可以以一种完全通用的方式完成,甚至不知道稍后将调用哪种类型的函数:

    // writer functions
    template<typename T> void write0(T msg) { std::cout << 0; };
    template<typename T> void write1(T msg) { std::cout << 1; };
    
    // global flag
    bool use_write0;
    
    // function pointers for all types
    template<typename T>
    struct dispatch {
       typedef void (*write_t)(T);
       static write_t ptr;
    };
    
    // main write function
    template<typename T>
    inline void write(T msg) {
       (*dispatch<T>::ptr)(msg);
    }
    
    // the fun part
    template<typename T>
    void autoinit(T msg) {
       if (use_write0)
          dispatch<T>::ptr = &write0<T>;
       else
          dispatch<T>::ptr = &write1<T>;
       // call again for dispatch to correct function
       write(msg);
    }
    
    // initialization
    template<typename T>
    typename dispatch<T>::write_t dispatch<T>::ptr = &autoinit<T>;
    
    // usage example
    int main(int argc, char **argv) {
       use_write0 = (argc == 1);
       write("abc");
       return 0;
    }
    

    对于T 的每种类型,对write&lt;T&gt;() 的第一次调用决定了应该使用哪个写入函数。之后的调用则直接使用指向该函数的函数指针。

    【讨论】:

    • 聪明。这正是我所寻求的——如何实现模板专用存储(对于函数指针的值)。关键是使用模板化的静态类字段!加上这些字段的自动延迟初始化 - 太棒了。谢谢。
    【解决方案2】:

    您也可以使用 Don Clugston 的 FastDelegates 标头。不会产生任何运行时开销,并且是真正面向对象的委托。虽然使用它们的语法并不完美,但它比摆弄原始函数指针要简单一些。

    【讨论】:

    • 除非我弄错了,否则我不相信 FastDelegates 在这里有用,因为我不想绑定到成员函数。我认为核心问题是模板专用存储(用于函数指针的值)。
    【解决方案3】:

    为什么不使用函数指针数组?

    #include <iostream>
    using namespace std;
    
    template<typename T> void write0(T msg) { cout << "write0: " << msg.name() << endl; }
    template<typename T> void write1(T msg) { cout << "write1: " << msg.name() << endl; }
    
    template<typename T> struct WriteSelector
    {
        static void(* const s_functions[])(T msg);
    };
    template<typename T> void(* const WriteSelector<T>::s_functions[])(T msg)=
    {
        &write0<T>,
        &write1<T>
    };
    
    unsigned write_index=0;
    template<typename T> void write(T msg)
    {
        WriteSelector<T>::s_functions[write_index](msg);
    }
    
    
    struct MsgA { const char *name() { return "MsgA"; } };
    struct MsgB { const char *name() { return "MsgB"; } };
    struct MsgC { const char *name() { return "MsgC"; } };
    struct MsgD { const char *name() { return "MsgD"; } };
    
    void Test()
    {
        write(MsgA());
        write(MsgB());
        write(MsgC());
        write(MsgD());
    }
    
    int main()
    {
        Test();
        write_index=1;
        Test();
        return 0;
    }
    

    【讨论】:

    • 这是等价的(但可以推广到>2个选项)。此外,fn ptrs(和例如 vtable 调度)表现出与分支相似的性能。在这两种情况下,流水线在知道下一条指令之前就停止了(分支等待的时间稍长),但分支只有两种预测的可能性。
    【解决方案4】:

    写入有两个变化轴:write0/write1 选择和 MsgA/B/C.... 选择。

    从概念上讲,这意味着您需要 write 函数的 NxM 实现。当然,如果添加了 write 实现,或者添加了消息类型,这会导致 resp。要添加 M 或 N 个额外功能。

    对于这两个轴,您可以选择是使用静态多态还是动态多态来实现它们。静态多态可以使用模板或使用函数覆盖来完成。

    这可以通过在每个类中创建具有 M 个写入函数的 N 元素类层次结构来完成。但它很快就会成为维护的噩梦。除非消息内容也是运行时多态的。但问题是关于消息的静态多态性。

    由于过于复杂而排除了运行时多态性(并且你不能有一个虚拟的模板函数,这会减少覆盖的冗长),我们需要实现一个小的类型调度例程,将运行时信息转换为编译-时间信息。

    更具体地说:使用 writer-to-use 模板化主要操作(在名为 Tmain 的示例中),并使用来自“真实”main 的正确模板参数调用它。

    这省略了“全局”选择变量的使用,但它面向对象且简洁。

        // twodimensionalpolymorph.cpp
        //
    
        #include <iostream>
    
        using namespace std;
    
        class Write0 {
            public: 
            template< typename tMsg > 
            void operator()( /*const*/ tMsg& msg ) { cout << "write0: " << msg.name() << endl; };
        };
    
        class Write1 {
            public: 
            template< typename tMsg > 
            void operator()( /*const*/ tMsg& msg ) { cout << "write1: "<< msg.name() << endl; };
        };
    
        struct MsgA { const char *name() { return "MsgA"; } };
        struct MsgB { const char *name() { return "MsgB"; } };
        struct MsgC { const char *name() { return "MsgC"; } };
        struct MsgD { const char *name() { return "MsgD"; } };
    
        // the Tmain does the real action
        //
        template< typename Writer >
        int Tmain( Writer& write, int argc, char** args ) {
    
            write( MsgA() );
            write( MsgB() );
            write( MsgB() );
            write( MsgD() );
    
            return 0;
        }
    
        // the main merely chooses the writer to use
        //
        int main( int argc, char** args ) {
    
            if( argc==1 )
                return Tmain( Write0(), argc, args);
            else
                return Tmain( Write1(), argc, args);
    
        }
    

    【讨论】:

    • 没有什么被排除在“过于复杂”之外;模板化的虚拟对象根本不是有效的 C++。您的方法很有趣,并且适用于某些情况,但我正在整个程序中调用 write(),并且希望避免到处传播模板参数。
    • 顺便说一句,你的代码中的小错误:你不能有右值引用(还)。
    • 我没有正确说明:如果你想要两个没有模板的变体“维度”(即写入类型和消息类型),你最终会得到 NxM 实现。这很复杂,但可以解决问题:捕获函数指针,并在运行时保持灵活性。
    • 感谢您指出虚拟模板的事情。显然我需要了解右值(愚蠢的借口:VC++ 不会抱怨:()
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多