随笔-90  评论-947  文章-0  trackbacks-0

目录:

C++ 下 Function 对象的实现(上)
C++ 下 Function 对象的实现(下)

起因在上一篇已经说过了。现在让我们直接进入主题。本文的目标是,让以下代码能顺利跑起来:

int intfun0()
{
    return 1;
}

struct _intfunctor0
{
    int operator()()
    {
        return 2;
    }

} intfunctor0;

struct Test
{
    int intmem0()
    {
        return 3;
    }

} test;

int main()
{
    Function<int ()> f1(&intfun0);
    Function<int ()> f1_(intfun0);
    Function<int ()> f2(intfunctor0);
    Function<int ()> f3(&test, &Test::intmem0);

    f1();
    f1_();
    f2();
    f3();

    return 0;
}

除了上述例子中显示的,还要支持有返回值的函数和没返回值的函数,以及有0个、1个、2个、……、MAX 个参数的函数,参数类型无限制。最后实现的 Function 对象仅仅可以执行就好。(至于是否可拷贝、是否可判断相等 等问题,都是小事,本文暂不考虑。)最后,Bind 概念也不在本文讨论范围之内。

对于这个问题,我们一开始考虑的可能是怎样统一三种不同形式。有两个选择,第一,使用 C++ 的多态机制,最后统一到基类指针的类型;第二,允许类内部有冗余变量以及必要的 Flag,用于判断是哪种形式的函数,要如何执行。这样看起来,第一种方案比第二种爽一点。于是,最初想到的实现有可能是这样的:

先定义一个虚基类:

template <typename R>
class FunctionBase0
{
public:
    virtual R Invoke() = 0;
    virtual ~FunctionBase0() {}
};

然后实现一个普通函数/仿函数的版本:

template <typename R, typename T>
class Function0 : public FunctionBase0<R>
{
public:
    R Invoke()
    {
        return m_Fun();
    }

public:
    Function0(const T &fun)
        : m_Fun(fun)
    {

    }

private:
    T m_Fun;
};

这里需要说明的是,如果是普通函数,T会被特化成 R() 或者 R (&)() 或者 R(*)(),取决于使用的时候传入 fun 还是传入 &fun。所以不必另外实现针对 R(*)() 的版本。Loki (姑且就以作品名称乎 Loki 的作者吧,他那个真名实在是太长)在他的书中称之为“做一个,送一个”。不过对于他书中所说的,我有一个疑惑。Loki 说传入 fun,模版参数 T 会被特化成 R (&)(),于是一切顺利。可是我在操作过程中发现 T 一直被特化成 R (),于是上述 class 中的 m_Fun 被认为是成员函数而不是成员变量。不知道是为什么,有知道者请不吝指教哈。因为以上原因,本文中我一直用 &fun 的形式对待普通函数。

再实现一个成员函数的版本:

template <typename R, typename T>
class MemberFunction0 : public FunctionBase0<R>
{
public:
    R Invoke()
    {
        return (m_pObj->*m_pMemFun)();
    }

public:
    MemberFunction0(T *pObj, R (T::*pMemFun)())
        : m_pObj(pObj), m_pMemFun(pMemFun)
    {

    }

private:
    R (T::*m_pMemFun)();
    T *m_pObj;
};

最后是一个包装类。如果你可以接受 Function<int> 表示 int(), Function<int, int> 表示 int (int),…,那么这里没有多少技巧可言。boost 的那个 function 使用的是函数签名作为模版参数,即 Function<int()>,Function<int (int)> 等形式。如果不太研究语法,可能会像我一样,一开始会对尖括号里的 int (int) 之类的玩意儿不太熟悉,觉得很牛逼。可是了解了以后,不过是个函数类型而已,没什么大不了的。Loki 的 Functor 的使用方式是 Functor<int, TYPELIST_0()>,Functor<int, TYPELIST_1(int)>。其中第一个模版参数始终是返回值,第二个模版参数是参数类型列表,Loki 使用了他创造的玩意儿 TypeList 使得所有函数参数只占一个坑,这在等下的支持多参数的扩展中能够带来一些美观。我比较喜欢 boost 的使用方式,让使用者直接以语言规定的形式填入函数签名,而不是一些额外的约定(“第一个模版参数表示返回值”,“第二个到最后的模版参数表示参数”,“第二个模版参数以 TypeList 形式表示函数参数”等)。

为了达到这个目标,我们要玩一些偏特化技巧。关于偏特化,我一直以来的肤浅认识都是错误的。我原以为,对于模版类:

template <typename T0, typename T1>
class Foo;

我如果特化其中一个参数 T1:

template <typename T0>
class Foo<T0, int>
{

}

我以为只有这样才叫偏特化,以为偏特化的过程总是减少模版参数的。而实际上,只要用某个/些类型占据原始模版参数的位置,就可以了。比如,对于上述 Foo,我可以特化一个 class<T0, std::map<U0, U1>>,消去一个 T1,而新增 U0、U1:

template <typename T0, typename U0, typename U1>
class Foo<T0, std::map<U0, U1>>
{

}

原来 T1 的位置被 std::map<U0, U1> 占据了,这也是偏特化。当然最后的模版参数数量也可以不变,如:

template <typename T0, typename U>
class Foo<T0, std::vector<U>>
{

}

以及

template <typename T0, typename U>
class Foo<T0, U*>
{

}

其中后者是实现类型萃取的主要方式。只要特化以后,这个类依然带有至少一个模版参数,就是偏特化。如果最后产生了 template<> 的形式,那就是完全特化。

回到我们刚才的主题,我们要提供给用户的是这样一个类:

template <typename Signature>
class Function;

其中参数 Signature 会被实际的函数类型所特化。但是我们只知道整体的一个 Signature 并没有用,我们必须知道被分解开来的返回值类型、参数类型。于是,引入一个偏特化版本:

template <typename R>
class Function<R ()>

这里使用 R () 特化原始的 Signature,引入一个新的参数 R。于是返回值类型 R 就被萃取出来了。实现如下:

template <typename R>
class Function<R ()>
{
public:
    template <typename T>
    Function(const T &fun)
        : m_pFunBase(new Function0<R, T>(fun))
    {
       
    }

    template <typename T>
    Function(T *pObj, R (T::*pMemFun)())
        : m_pFunBase(new MemberFunction0<R, T>(pObj, pMemFun))
    {

    }

    ~Function()
    {
        delete m_pFunBase;
    }

    R operator ()()
    {
        return m_pFunBase->Invoke();
    }

private:
    FunctionBase0<R> *m_pFunBase;
};

如果对上面说的“普通函数的使用方式必须是函数指针而不是函数本身”耿耿于怀,可以再引入一个的构造函数:

typedef R (FunctionType)();

Function(const FunctionType &fun)
    : m_pFunBase(new Function0<R, FunctionType &>(fun))
{

}

这里 FunctionType 是 R(&)() 类型,强制使用它来特化 Function0 中的 T。该构造函数在重载决议中会取得优先权从而使普通函数本身的传入成为可能。不过,以函数本身形式传入的普通函数会丧失一些特性,比如 Function<int()> 只能接受 int() 类型的普通函数而不能接受 char () 型的普通函数,因为这种情况下不会走我们刚才新定义的构造函数。

还有一种做法,就是针对全局函数,强制特化出模版参数为其引用类型的类。定义如下元函数:

template <typename Signature>
struct FunctionTraits
{
    typedef Signature ParamType;
};
   
template <typename RetType>
struct FunctionTraits<RetType ()>
{
    typedef RetType (&ParamType)();
};

然后构造函数改为:

    template <typename T>
    Function(const T &fun)
        : m_pFunBase(new Function0<R, typename FunctionTraits<T>::ParamType>(fun))
    {
       
    }

用以上方法,所有的特性都不会丢失。

到这儿,我们的 Function 已经可以小试牛刀了:

Function<int ()> f1(&intfun0);

Function<int ()> f1_(intfun0);
Function<int ()> f2(intfunctor0);
Function<int ()> f3(&test, &Test::intmem0);

f1();
f1_();
f2();
f3();

上面这段代码已经能够正常运行了。

来,继续做一个,送一个。下面的代码居然也能跑(voidfun0、voidfunctor0、Test::voidmem0类似int版本定义):

Function<void ()> f4(&voidfun0);
Function<void ()> f4_(voidfun0);
Function<void ()> f5(voidfunctor0);
Function<void ()> f6(&test, &Test::voidmem0);

f4();
f4_();
f5();
f6();

这说明了,在类里面写一个返回值为该类型的函数,并在里面写下 return XXX; 然后以 void 为模版参数传入该模版类,是符合语法的。验证一下:

template <typename T>
class Foo
{
public:
    T Bar()
    {
        printf("%s invoked\n", __FUNCTION__);
        return T();
    }
};

int main()
{
    Foo<void> f1;
    f1.Bar();

    Foo<int> f2;
    int i = f2.Bar();

    return 0;
}

运行结果:

Foo<void>::Bar invoked
Foo<int>::Bar invoked

到此为止,我们已经实现了 0 个参数的函数支持,也即 R () 类型的所有函数的支持。接下来还要实现对具有 1 个、2 个、3 个直至任意有限个参数的函数支持。也许您也发现了,接下来的工作可以是体力活,我们可以照葫芦画瓢,搞出一堆 FunctionBaseN、FunctionN、MemberFunctionN,并在最后的 Function 中再实现 N 个偏特化版本。是,不错,大致上原理就是这样。限于篇幅,我想暂时写到这里,下篇将继续谈谈宏、TypeList,以及怎样少花点力气实现其余 N 个版本。最终达到的效果是,只要改一个宏定义,就可以提高参数上限。

在本文所涉及的内容中,我比较纠结的是,可否在不用多态机制的情况下达到比较优雅的形式统一?

欢迎讨论。

posted on 2011-01-16 22:17 溪流 阅读(7269) 评论(55)  编辑 收藏 引用 所属分类: C++

评论:
# re: C++ 下 Function 对象的实现(上) 2011-01-16 22:32 | zhaoyg
帮顶  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-16 22:43 | cexer
不说MFC那种陈旧的消息机制,用boost::function,boost::bind,boost::signal之类的来实现消息机制,也算有点out了,而且这种实现已经有框架使用(比如SmartWin++),不再具有新意了,其实强大的C++可以完成远比它们更高级的功能,列举三点:

1 框架可以给同一个消息源添加任意多个个完全不同的消息处理器,类似函数重载,可以将这个功能命名为处理器重载。仅仅是类似boost:function和boost:signal那种固定签名的工具,是无法完成这样的功能的,如下所示:

// 自由函数
void global_handler(){}

// 一段脚本
Script script_handler = create_script( /*这是一段脚本*/ );

// 系统函数
int WINAPI MessageBox( HWND,LPCTSTR,LPCTSTR,UINT );

// 具有不同参数的成员函数
viod Class:memeber_handler_void(){}
void Class::member_handler_size( Size size ){}
void Class::member_handler_int( int x,int y ){}

// 消息映射
Window window;
Class object;
window.on_resized += message_handler( global_handler );
window.on_resized += message_handler( script_handler );
window.on_resized += message_handler( &Class::member_handler_void,&object );
window.on_resized += message_handler( &Class::member_handler_size,&object );
window.on_resized += message_handler( &Class::member_handler_int ,&object );
window.on_resized += message_handler( &::MessageBox,NULL,"Hello World",MB_OK );


2 框架能够进行返回值检查(众所周知的,在windows消息系统中,很多消息需要进行特殊的返回值检查,强WM_ERASEBKGND之类的),比如一个closing消息,如果处理器返回非void类型,并且能够强制转换为true,则表明它同意关闭窗口,如果能够强制转换为false,则表示它阻止框架进行关闭,如果处理器返回值类型是void,则表示它不关心关闭与否,让框架进行默认处理。这个功能可以命名为返回值智能检测。如下所示:

// 这个处理器不关心窗口是否要关闭
void handle_closing_dont_care( Window* window )
{
}

// 这个处理器告诉框架不要关闭
LRESULT handle_closing_no()
{
return 0;
}

// 这个处理器告诉框架进行关闭
bool handle_closing_yes()
{
return true;
}

// 消息映射,窗口最终是否关闭取决于最后一个处理器的决定。
Window window
window.on_closing += message_handler( handle_closing_dont_care );
window.on_closing += message_handler( handle_closing_no );
window.on_closing += message_handler( handle_closing_yes );


3 框架提供消息分解者,允许使用者自己组合自己的处理器参数,类似boost::asio的handler绑定bytes_transfered和error_code的方式,可以把这个功能命名为消息参数任意组合,比如同一个消息resized,有的处理器需要当前窗口的大小,有的处理器需要当前窗口的标题,有的处理器同时需要两者,则消息映射时手动组合消息分解者即可。更进一步,对于某些预注册的签名,框架可以智能提供分解者,免去使用者手动提供的麻烦,例如签名 (Window*,Size) 已经预先注册,则具有这个签名的处理器不用手动指定消息分解器
示例代码如下:

// 这个处理器需要当前窗口大小
void handle_resized_size( Size size ){}

// 这个处理器需要窗口标题
void handle_resized_text( String caption ){}

// 这个处理器都需要
void handle_resized_both( Size size,String caption )


// 消息映射的时候,同时指示自己需要什么参数
Window window
window.on_resized += message_handler( handle_resized_size,Window::Argument::size )
window.on_resized += message_handler( handle_resized_text,Window::Argument::text )
window.on_resized += message_handler( handle_resized_both,Window::Argument::size,Window::Argument::text )


void handle_resized( Window* window,Size size ){}
window.on_resized += message_handler( handle_resized );

4 但是。。。。。在C++.0x.lamda的面前,以上一切都成了浮云。把lamda作为消息处理器,它自身就同时提供了以上1,2,3的所有优点,简洁优雅使用方便,物美价廉童叟无欺,以上例说明:

// 这个处理器需要当前窗口大小
window.on_resized += []( Size size ){
}( window::Argument::size );

// 这个处理器需要窗口标题
window.on_resized += []( String caption ){
}( Window::Argument::text );

// 这个处理器都需要
window.on_resized += []( Size size,String caption ){
}( window::Argument::size,Window::Argument::text );

我正在写的就是这么一个框架,上面列举的消息机制都完成了,正在进行博主所指第4步封装控件和第5步(写文档,作为一个目标是开源的框架,是最重要的步骤),目前代码已经有两万多行,消息机制的其实占很少部分,大多代码是控件的封装。

  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-17 12:37 | 飞舞的烟灰缸
@cexer
function的多播实现还是有点麻烦。
+= 倒是简单,但是要实现 -=这样的操作就需要判断function相等,麻烦就比较大了  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-17 12:52 | 溪流
@飞舞的烟灰缸
求不借助于typeid的function相等的判别方法~  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-17 12:53 | 溪流
@cexer
很有启发性的提示,谢谢分享~!顶了再细读。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 02:01 | OwnWaterloo
typelist 不给力的, 它没办法展开为一个参数列表。
最终还是需要boost.pp 那样的东西。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 02:17 | OwnWaterloo
借宝地请教一下~ 对于handler, 是否需要如此的动态?

A. 完全动态

比如, 一个具体的 window 类。
window w; // 具体的一个类
w.add_handler( ON_CREATE, my_on_create, ... ); // 动态的添加一些handler。

具体语法不重要, w.add_handler<ON_CREATE>(my_on_create, ...);
关键点是, window是一个具体类, 动态的(运行时而非编译时)添加一些handler。

if (some_condition)
/*run time*/w.add_handler( ON_CREATE, my_on_create, ... );


B. 完全静态

与之相对的, 是编译时就决定一个类型, 比如 my_window。
然后让它处理 ON_CREATE。 如何让WndProc分派到这个handler同样不是重点。
重点是my_window的每个instance肯定都会处理这个event, 没有例外。


C. 两者结合, 动态部分自己实现。

两者的折中可能是这样:
1. 依然是一个具体的, 编译时决定处理哪些event的window类, 比如叫y_window
2. 在设计y_window时, 认为有"部分"(而非window那样的全部)行为需要在运行时定制
3. 在event中留一些stub, 用于运行时添加。

比如 y_window::on_create( param ){
/*同样, wndproc 如何分派到这里不重要*/
/**/for (it=this->on_create_handlers.begin(); it!=this->on_create_handlers.end(); ++it) *it(param);
}
y_window::add_on_create( f , ... ) {
/**/this->on_create_handlers.push_back(f, ... );
}


如上, 需要A那种灵活性么? 缺之不可?
因为我自己确实没写过几个gui程序, "作为一个库的用户", 没有足够的这方面的使用经验。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 11:47 | yrj
@OwnWaterloo
动态方式解耦库的实现与库的用户,可看看下面这篇文章
http://www.elpauer.org/stuff/a_deeper_look_at_signals_and_slots.pdf  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 16:53 | zhaoyg
"可是我在操作过程中发现 T 一直被特化成 R (),于是上述 class 中的 m_Fun 被认为是成员函数而不是成员变量"

这个会不会是编译器理解成你试图在定义一个函数的变量,于是才...


  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 16:56 | zhaoyg
如果 m_Fun是T* 而不是T , 可能就没有这个问题了  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 17:28 | cexer
@OwnWaterloo
【借宝地请教一下~ 对于handler, 是否需要如此的动态?
A. 完全动态
B. 完全静态
C. 两者结合, 动态部分自己实现。
如上, 需要A那种灵活性么? 缺之不可?
因为我自己确实没写过几个gui程序, "作为一个库的用户", 没有足够的这方面的使用经验。】


说说我的看法:A并不是必须的,比如说MFC,可以算是完全静态的,其所有的消息映射代码都是用宏堆死的选择语句,提供的动态功能基本上无,优势很明显,一个类的所有实例共享一份映射数据,具有空间优势。但是一个独立的对象不能有它自己处理事件的方式,这明显不是面向对象的封装方式,运行时灵活性大打折扣,这样说起来很模糊,仍然举例MFC来说明:

比如说使用MFC运行时创建的一个按钮,怎么样才能把这个按钮的消息加到消息映射里去呢?对不起,不能。因为按钮ID是动态生成的,那些映射宏什么的到运行时都成了浮云。解决办法当然也有:回到原始时代--要么在父窗口的消息回调函数里检查WM_COMMAND和BN_CLICK,二是任劳任怨地去自己重写一个按钮类,并在自己的消息回调里响应WM_COMMAND的反射消息并检测BN_CLICK,然后进行处理--看看有多麻烦。

所以我觉得一个好的库,应该能够同时提供两者,就是你说的C。同时支持静态和动态的映射方式,关键是库要实现得好,在没有使用动态映射的时候,这个动态映射的功能就好像不存在一样,这样大多数时候既能够享受空间优势,在需要的时候又能发挥它的动态优势。免得用户要在在空间优势和灵活性之间做出痛苦的选择。
  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 17:38 | cexer
@飞舞的烟灰缸
【function的多播实现还是有点麻烦。
+= 倒是简单,但是要实现 -=这样的操作就需要判断function相等,麻烦就比较大了】

我想你指的麻烦,可能是你想实现这样的功能:
// 加入
on_event += message_handler( handler );
// 删除
on_event -= message_handler( handler );

// 加入
on_event += message_handler ( &Window::handle_resized,this );
// 删除
on_event -= message_handler ( &Window::handle_resized,this );

这样的方式实现其实也不难,但是绝对需要 RTTI 那种东西的支持,另外还有一个问题就是这样的方式使用用起来太麻烦,比如:

// 加入
on_event += message_handler ( &Window::handle_resized,this );
// 删除
on_event -= message_handler ( &Window::handle_resized,this );

如果库提供了类似 boost::asio 那种参数绑定的功能,看看有多麻烦:
// 加入
on_event += message_handler ( &Window::handle_resized,this,Argument::size,Argument::caption );
// 删除
on_event -= message_handler ( &Window::handle_resized,this,Argument::size,Argument::caption );


其实这个功能有比你个简单得多的实现方式,给一个 handler 绑定一个名字,到时删除这个名字即可,见下例:
// 加入
on_event += message_handler ( name,handler );
// 删除
on_event -= message_handler( name );


  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 17:59 | cexer
实现一个functor,除了众所周知的功能,我建议楼主再考虑以下这两点:

1 给返回void(无返回值)的functor绑定一个有返回值的可调用体,这个貌似boost::function是支持的。这功能是需要的,因为实际上很多时候,我们的functor不关心返回值,就好像某个消息无论你返回什么系统都执行相同的操作,这种情况下,如果绑定的可调用体返回了什么值,functor应该检测到并使用某种屏蔽手段,而不是报错“对不起,返回类似不匹配”。

2 给返回非void(有返回值)的functor绑定一个返回void(无返回值)的可调用体,这个貌似boost::function不支持。这功能是需要的,因为有时候functor需要一个明确的返回值,就像WM_ERASEBKGND消息,根据返回TRUE或FALSE的不同,系统会决定是否自己来画窗口背景。如果可调用体返回void(无返回值),那么functor应该检测到它,并在返回时提供一个默认的返回值,而不是在绑定时就报错“对不起,返回类似不匹配”。

以上两点都可以通过模板元编程实现,通过typetraits检测返回值,并根据functor和可调用体之两者返回值的不同组合,来选择不同的返回策略。

另外还有,如果想绑定系统函数如Windows API,或者其它第三方提供的函数,你还需要考虑调用约定的问题,因为 __stdcall,__cdecl,__fastcall的不同,都会使函数签名不同。有多少种调用约定,functor的绑定函数的数量就需要乘以多少,这是个体力活,可以用预处理元和文件包含来减少体力消耗。
  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 18:27 | OwnWaterloo
@cexer
感谢感谢~ 果然是经验丰富的大神。

我想说的就是对象模型不同导致的"空间和效率"上的差异。
C++ 其实不是"面向对象"语言, 而是"面向类" 语言。
真正的面向对象语言, 每个instance都是相互独立的。
而C++,每一个type的所属instance共享一部分行为。


C++其实就是将"可能拥有一组相同行为"的instance们, 归属到同一个类里,
共享许多信息, 并提高空间与时间效率。
但是, 当instance们的行为复杂(灵活)到一定层次时, 极端情况, 就是一个class, 只需要一个instance。


这时候, 如果依然遵守静态类型的桎梏, 为每种行为组合编写一个类, 就会非常繁琐, 远不如动态类型语言来得方便。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 18:37 | OwnWaterloo
@cexer
上面所说的"一个类只需要产生一个instance"是非常极端的情况。
实际上是否真的会产生这样的情况?


比如那个动态产生button的问题。
如果依然遵守静态类型的方式, 既然"预见" 会产生多个button, 是否可以先在on_command里留下stub, 然后把on_command这部分动态化。
每个button被按下的行为不同, 但这些行为是否依然是"可预见"的?
是否依然可以将它们归入到同一类型中, 比如使用类多态机制?


"A完全动态" 的方案给我的感觉是: 这已经不是在用C++, 而是在C++中实现一个微型动态类型系统, 然后用动态类型的方式编程。
如果是我个人作出选择的话, 当需要这种灵活性时, 直接把C++丢了, 选其他动态类型语言来编程-_-  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 18:39 | 溪流
@zhaoyg
是的,T*自然没问题,就是为了同时也支持传入 T。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 18:51 | OwnWaterloo
@cexer
>> 同时支持静态和动态的映射方式,关键是库要实现得好,在没有使用动态映射的时候,这个动态映射的功能就好像不存在一样,这样大多数时候既能够享受空间优势,在需要的时候又能发挥它的动态优势。免得用户要在在空间优势和灵活性之间做出痛苦的选择。


说得我口水都流了…… (其实是开饭了~)
有没有机会能先睹为快呢~


假设, 这部分能做到上述所说。
但还是有别的问题。

比如参数绑定(boost.bind), 在其他范畴里叫partial apply。
其他语言要实现这个简直太容易了, 语言直接支持, 不需要一个额外的库。
也不需要关心被bind的参数的生命周期问题。

比如匿名函数, boost.lambda应该也是有不少限制的, C++0x目前也不算太成熟 —— 还有这么多人用VC6呢…… 能让他们进化到VC8就很感谢了, VC10感觉是奢望。


还有你上面提到"给handler一个名字, 以方便从handler们中删除" —— 这完全就是动态类型语言常用的方式嘛……
比如lua:
handlers = {}
添加:
handlers[name] = handler
调用:
for _,v in pairs(handlers) do v( ... ) end
删除:
handlers[name] = nil


还有楼主提到的typelist, 其实是无法展开为参数列表的。
这功能在动态类型语言里也太容易了……
最简单的元编程方式, 就是产生所需代码的字符串, 然后加载之……


总之呢, 当需要灵活性时, C++就缺这缺那的, 什么都要自己去造……
造的时候, 又有各种问题。


造的时候肯定是很有成就感的, 编程的乐趣啊!
但就我个人而言, 已经懒惰了……  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 18:58 | OwnWaterloo
@cexer
来说一个具体的C++问题吧, 就是这个:
>> 你还需要考虑调用约定的问题,因为 __stdcall,__cdecl,__fastcall的不同,都会使函数签名不同。有多少种调用约定,functor的绑定函数的数量就需要乘以多少,这是个体力活,可以用预处理元和文件包含来减少体力消耗。

绝对的体力活啊! 干过一次绝对不想干第二次……

而且, 你有试过g++么?
g++中这问题很麻烦。
函数重载目前是通过mangling实现的, 而g++对一个函数(free or member)指针类型的mangling只包含普通的signature, 不包含调用约定。

伪代码:
binder bind( __cdecl f, arg )
binder bind( __stdcall f, arg )

俩f在g++中是不同类型, boost::is_same是false。
但这俩bind函数"不构成重载" , 是bind的重复定义……
原因就是上面提到的, f被mangling时, 它的调用约定信息被忽略了。
俩bind最终产生的符号是相同的。


用C++, 就要和这些许多细枝末节的问题作斗争……
懒了…… 想逃避之……  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 19:24 | OwnWaterloo
@cexer
同时, 我还发现C++程序员很容易产生一种倾向:
嗯, 我要设计一个程序库, 这个库很smart, 无论怎么使用, 都是可以的。

想怎么用, 就怎么用 —— 以前我也是这样想的。


就举这个帖里出现的一个例子: 某处理器需要一个返回值。 是否可以让程序员在需要默认返回时, 编写一个返回void的处理器?

就我个人而言 ( 仅仅是个人的看法, 与技术无关了, 两者都是对的), 类似WTL那样也是"完全可用"的。
... frame work ...
case WM_XXX :
R result = default_result;
user_handler(&result, ... );
return result;

签名必须写为:
(R* response, ... ); // 其实是个引用, 只是指针的话, 上面的frame work中的代码意图更明显。

"有且仅有这一种做法" , 没有第二选项。
若用户不关心, 不去修改response的值即可。


其实我也觉得传出参数是很难看的设计。
但它可工作, 也可以避免去编写一堆meta programming 代码, 而且这堆代码可能又需要同C++的各种阴暗作斗争。
例如boost, 若不需要"照顾" 这么多编译器的话, 其实代码还是可读的。
但需要照顾这么多编译器, 那无数的workaround, 使得代码变得……
不是天书也是烂泥……



"嗯, T是一个原始指针可以, 当它是个smart pointer时也应该要被支持"。
很多时候, C++ 程序员不是去考虑这种需求是否存在, 实现这种需求的复杂度如何, 维护代价如何。
仅仅是因为他们能, 所以他们就要这样做……

其实有时候, 这些需求仅仅是美学上的, 而不是技术上的。
比如上面那个返回值的例子。
再比如:

f(char const* s, ... ); // primary function
f(std::string const& s, ... ) { return f(s.c_str(), ...); }
f(CString const& s, ... ) { return f(static_cast<char const*>(s), ...); }
...

如果不要后两者, 任何工作依然可以完成。
后两者仅仅是提供一种方便, 而不是"核心"接口。

也不是说这种方便不好…… 我的观点是一定需要有某种方式告之用户, 哪部分是核心接口。
用户优先去掌握这部分, 然后整个库的功能他就可以完全掌握了。
其他的, 学得越多, 使用越方便。

否则, 若没有这种通告, 一个f不要紧, 若全体函数都这样搞, 就会增加学习成本。
本来15个核心函数, 如果扩展为45个, 75个, 就不仅仅是量上的变化, 而是质上的变化 —— 让人失去耐心。
这同为什么要抑制warning一样, 不是说不可以产生warning, 而是说当warning多到一定程度时, 会超过人的处理极限, 会不自觉的将他们全体忽略掉。
所以发现warning就要干掉, 让新的warning能引起人注意。


至于通告方式…… 感觉手写文档是最靠谱的……
docxgen 什么的…… 产生的文档太死气沉沉, 无法感受到当中的轻重缓急。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 19:37 | OwnWaterloo
举个实际例子 (我就一话痨……) :
最近在教人C语言……

条件包含部分: #if, #elif, #else, #endif, defined 就完全ok了。
所有功能都可以实现。
但C语言还提供了 #ifdef, #ifndef…… 而且人们通常用的就是这个方便的接口……
而且很多时候连#elif, #else都没有 —— 比如头文件保护符, 我就是在给别人解释这个事情。

为什么有#ifdef, #ifndef后, 还要有 #if 与 defined?
若不给初学者解释 #elif , 是肯定理解不了的。


如果C语言一开始就只为条件包含提供一种方法: #if, #elif, #else, #endif, defined。
大家也就只能这么写。
那介绍头文件保护符时, 就先介绍#if, defined, #endif就完了, 暂时不需要牵扯出其他部分, 其他部分可以慢慢了解。
人脑没法一下子容纳太多信息的。



再声明一下, 以上都是个人观点, 话痨犯了, 聊聊天而已。
完全没有"其他人也必须这样", "这就是事实"的意思……  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 19:39 | OwnWaterloo
@yrj
感谢~
其实我困惑的不是"如何实现", 而是"是否需要在C++中实现"。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-18 23:49 | 溪流
@cexer
@OwnWaterloo
关于调用约定,貌似不用复制N份(VS2010下测试),函数会自动被识别为 R (__stdcall *)(...)  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:00 | 溪流
@OwnWaterloo
很多时候, C++ 程序员不是去考虑这种需求是否存在, 实现这种需求的复杂度如何, 维护代价如何。
仅仅是因为他们能, 所以他们就要这样做……
其实有时候, 这些需求仅仅是美学上的, 而不是技术上的。

“仅仅是因为他们能, 所以他们就要这样做……”有时候确实这样,但一般实现代价一般很小的情况下才会出现这种冲动,想要顺手做了。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:17 | 溪流
@cexer
谢谢分享这么多想法。

给 handler 起名字确是删除 handler 的好办法,我可以不用纠结要不要用 typeid 啦。

另外关于你说的不同类型的handler的绑定,作为 Function,我觉得不该加入这样的特性;作为GUI专用的Event,是可以接受有的特性的。

但是另一点疑问是,框架告诉用户的规则——“返回 void 表示让框架决定是否继续处理,返回bool表示用户想让框架继续处理或者拒绝用户继续处理”,与WTL的bHandled告诉用户的——“设置bHandled用于指示框架是否继续处理”相比,优势大吗?都存在一种约定。而用返回void与bool的形式,可能仅仅是更加炫一点?

实现上,不管用户看到的参数是怎样的,框架最初获得的肯定是 WPARAM 和 LPARAM,这样,就算不修改 Function,似乎也可以在控件中给出不同的处理函数形式,例如给出一组重载的 OnClick:
OnClick(Function<void (int, int)>)
OnClick(Function<bool (int, int)>)
又例如给ClickEvent实现重载的operator+=?

前两天我在犹豫框架要不要帮用户解析 WPARAM、LPARAM,你的例子让我坚定了想法,解析之!。。让使用者不需要查 MSDN。。。或许做不到这种程度吧。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:22 | OwnWaterloo
@溪流
貌似你误会了。

cexer所说的应该是这个情况:
比如需要实现bind(或者是别的功能, 这个不是重点, 只是用于举例)。
重点是: 库的用户会用不同的调用约定的函数传入。

还是上面的伪代码:
binder bind( f , arg ) { ... }

R0 __cdecl F0_of_client ( ... );
R1 __stdcall F1_of_client ( ... );

bind(F0_of_client, ... );
bind(F1_of_client, ... );

只有两者之一会成功。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:24 | 溪流
@OwnWaterloo
欢迎聊天~~~

warning神马的,感觉就是要见一次杀一次,不然绝对破窗效应。

“当需要这种灵活性时, 直接把C++丢了,选其他动态类型语言来编程。”有一群纠结的人(当然不是我),他们的信仰就是用 C++ 做任何事,或者说他们的兴趣就是用C++ 来做任何事,那怎么办呢?呵呵。且不说个人兴趣导向。从很理性的结果主义的角度考虑,这种时候确实得换,应该要换,但也不是每个这样的场合都有条件换,比如团队里没有会python的且不想学python,但有一群蛋疼的Cpper,这种事情也会发生吧。

最后,你说你在教C?求教,求升华~!·^_^  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:25 | OwnWaterloo
@溪流
>> 但一般实现代价一般很小的情况下才会出现这种冲动,想要顺手做了。

以前是不管代价如何就很冲动去顺手了……
现在想来……
也算是练手吧……
  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:29 | OwnWaterloo
@溪流
>> 前两天我在犹豫框架要不要帮用户解析 WPARAM、LPARAM,你的例子让我坚定了想法,解析之!

解析WPARAM和LPARAM确实无比无比蛋疼啊……
当然, 最初的那些C程序员也不是没办法了, 有个 windowx.h 里面的宏有一些帮助。

我也觉得, 作为一个有责任的库, 应该做之。
如果能提供没有解析的原始层次, 供那些有经验的程序员使用, 似乎也可以?


似乎程序员都有实现gui的冲动?
我也冲动过的…… 但就是被WPARAM, LPARAM击败了……
本想以此当作熟悉各种消息的方式, 最终还是没坚持下来……
没毅力的人飘过……
  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:31 | OwnWaterloo
@溪流
嗯嗯, 这个确实符合破窗效应。

但15个api与45、 75个api应该用什么效应解释呢?
反正就我的感觉, 15个会很有耐心的去看; 而45个就没耐心了; 75个就绝对不想去看了, 并给自己一个心理安慰:这玩意太复杂, 没设计好……  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:33 | 溪流
@OwnWaterloo
bind的时候会遇到怎样的情形我目前不知道。但是就 function 的阶段来说,我刚刚自认为比较仔细地确认了代码能够跑的样子:

void __cdecl f1(int)
{

}

void __stdcall f2(int)
{

}

int main()
{
Function<void (int)> g1(&f1);
Function<void (int)> g2(&f2);
g1(0);
g2(0);

return 0;
}

观察汇编结果,f1的ret出来后有 add esp 4动作,f2的ret 4出来后没有  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:34 | OwnWaterloo
@溪流
嗯, 理想是一回事。 有时候还是得向现实屈服……
作为个人的话, 还是可以学学python(perl, ruby) 什么的, 日常编程工作就可以摆脱C/C++了……  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:35 | OwnWaterloo
@溪流
你会C++还怕C么……

说是教, 也就是以前的死党指点指点。
都是灰常基础的东西……  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:47 | OwnWaterloo
@溪流

template<typename R>
R call( R (*f)() ) { return f(); }

void __cdecl f() { printf("cdecl\n"); }
void __stdcall g() { printf("stdcall\n"); }

VC8默认情况: call(f) 是可以的, 而 call(g)不行。
因为 call 中的 f 参数没有指定调用约定。
如果显式使用 /Gr , 那就call(f)不行, call(g) 可以。



如果使用重载:
template<typename R>
R call( R ( __cdecl *f)() ) { return f(); }


template<typename R>
R call( R ( __stdcall *f)() ) { return f(); }

两者都可以, 但在g++下, 只要两者被同时使用, 就是重定义。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:50 | 溪流
@OwnWaterloo
多学点自然是应该的,但我觉得不应该追求日常编程脱离C/C++啊。。。那是件多么可怕的事,关系到饭碗。当然不是歧视脚本语言什么的,但是从现状看来,越来越多的人在用更加简单的语言,业界最稀有的的还是C/C++的人啊。(vczh说的生存斗争观点我很赞同。)越简单意味着入门越容易,竞争者多,于是想胜出就难了。所以我觉得往复杂方面靠的大方向还是要的。所以。。。用API不如造API,用轮子不如造轮子,不如用造轮子的轮子。。。临了的时候,也许可以居高临下的跟别人说:你,不就是会很多API么?这些API我都造过。你,不就是会用很多轮子吗?这些轮子我都造过。你,不就是会用很多语言吗?这些语言我也都造过。。。哈哈,扯淡了  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:55 | OwnWaterloo
@溪流
误会误会…… 我说的"日常"是指工作饭碗以外的……

比如, 用metablog 转移blog文章, 好像就是你写过的吧?
这用C/C++就蛋疼了……

就是一些个人事务, 有重复与机械的味道, 而用C/C++去做太麻烦。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:56 | 溪流
@OwnWaterloo
但是只要不要把返回值拆出来,就是可以的

template<typename F>
void call(F f ) { return f(); }

void __cdecl f() { printf("cdecl\n"); }
void __stdcall g() { printf("stdcall\n"); }
  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 00:59 | 溪流
@OwnWaterloo
呵呵,那玩意儿我本想改成一个博客客户端,一直没动。。。
可是,工作范围以外,就更要玩感兴趣的语言了呀~
我写转移文章的东西,不是为了转移文章,是为了写。
当然,碰到一些具有真实需求的并且有紧迫性的事情,那是要追求效率的。。。只不过这种状况跟工作无异了  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 01:15 | OwnWaterloo
@溪流
嗯, 这是一个绕开的方法, 然后通过别的机制去获得F的返回类型与参数列表还有调用约定。

在我的那个case里, 返回类型、参数列表、调用约定都要获取, 并对不同的调用约定做不同的事。
嗯, 就是thunk啦, 你懂的。

最终还是要获取那些信息, 所以我直接用
template<R>
R call ( R (*f) )
来获取了。 这机制叫啥名一下子想不起来了……


但这对g++依然不行。
产生的两个函数相互依然是重定义。
  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 01:18 | OwnWaterloo
@溪流
举个例子嘛……

不行就再举个, 比如登录, 批量下载图片……
嗯, 就是校内…… 我就是因为这个学python的。

最开始是打算用 libcurl, 但发现C/C++处理字符串太蛋疼……
又打算将 libcurl绑定到lua, 算练手(因为那时候python还不熟)
最终发现还是麻烦, 直接用python了……

嗯, wget 什么的我不熟……
用python做也算顺便练习吧……  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 01:22 | OwnWaterloo
就批量下载图片这个case来说, C++能提高什么效率?
网络传输才是大头, 这C++是提升不了的。
也就提高解析html的效率…… 但那是多么蛋疼的事情……

5秒传输是死的。
0.5秒python解析, 与0秒(算极端情况)C++解析
体会不到差异……
  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 01:23 | 溪流
@OwnWaterloo
呵呵

这种写法

template <typename S>
class C;

template <typename R>
class C<R (__stdcall)()>
{

};

发现通不过。。。

还有这个:

template <typename T>
class C;

template <typename U>
class C<SomeTraits<U>::Result>
{

};

偏特化的尖括号里不能进行摸板元运算?


  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 01:24 | 溪流
@OwnWaterloo
嗯,从实际角度上来说确实这样~~  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 01:28 | OwnWaterloo
@溪流
template <typename S>
class C;

template <typename R>
class C<R (__stdcall*)()> {};

template <typename R>
class C<R (__stdcall&)()> {};

"函数类型"是一个很灰色的地带……  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 10:50 | 飞舞的烟灰缸
@cexer
妙啊,想不到解决方案这么简单。还为此郁闷了很久呢。只是看起来稍微有点冗余,我这样有代码洁癖的人的确不太容易想到。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 11:15 | 飞舞的烟灰缸
@溪流
前两天我在犹豫框架要不要帮用户解析 WPARAM、LPARAM,你的例子让我坚定了想法,解析之!。。让使用者不需要查 MSDN。。。或许做不到这种程度吧。

这也是一个两面的问题。用户自己解析他至少可以去查MSDN,熟悉GUI的用户可以很方便的上手;框架提供解析那要做到怎样的程度呢,如果全部由框架解析那至少等大部分常用的消息都解析完成后这个框架才跑的起来,之后还要不断的完善直到把所有非常用的消息也解析完毕,这是一个浩瀚的工程。
一个折中的办法是提供常用消息的解析,同时用户可以自己得到并处理WPARAM、LPARAM,就像WTL的用法。我自己反而觉得一堆使用WPARAM、LPARAM相同参数的处理函数看着整齐更赏心悦目。
  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 11:18 | 飞舞的烟灰缸
GUI框架的复杂度主要是来源于实际要处理的问题的复杂度,这个是现实世界的复杂度没办法的事。框架做的少用起来复杂,框架做的多了学起来复杂用起来也未必简单多少,个人观点。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 12:32 | 溪流
@飞舞的烟灰缸
我觉得做或者不做都是一个方案,但是不同意折中。如果折中,框架没法跟用户解释,为什么WM_LBUTTONUP做了额外的工作,WM_DESTROY就没做呢?框架没有理由认为哪些是常用的。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 16:16 | yrj
@OwnWaterloo
共同提高吧。

我觉得是需求的多样性导致了实现的多样性。虽然 C++ 语言不太适合动态消息绑定,但在有些情况下有这样的需求,有这样的需求就有人用 C++ 实现这种功能。

曾看到另外一篇相关的文章
function/bind的救赎
http://blog.csdn.net/myan/archive/2010/10/09/5928531.aspx  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 18:47 | OwnWaterloo
@yrj
孟岩的是吧?
当时这篇文章在buzz上被分享是我就不予好评。
理由就是上面和cexer提到的, 完全的动态, 纯面向对象而不是面向class是有代价的, 而且是C++必定不能承受的代价。
只知道说C++这不好, 那不好, 也不想想为什么。
他的文章(至少C++相关那些)在我看来都是水多料少。
也许时代不同了吧……  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 22:19 | yrj
受教了。

我并不执着于"是否需要在C++中实现"动态或者"C++必定不能承受的代价"。我想要知道的是每种方法的优缺点和它的适用条件,应用时根据不同的需求选择适合的方法。在嵌入式GUI应用中强调运行效率及省电等情况下,会采用完全静态的方法;如运行效率等条件可以放松的情况下,则采用一些动态的方法提升开发效率。
  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 22:28 | OwnWaterloo
@yrj
>> 我想要知道的是每种方法的优缺点和它的适用条件,应用时根据不同的需求选择适合的方法。

嗯, 这是王道。
根据问题选合适的方案, 而不是将自己熟悉的方案去套各种问题。

关键是, 孟岩只提一方面(灵活性) 不提其代价。 片面的给C++抹黑。
仿佛能给C++抹黑就显得自己多高水平似的。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-19 23:24 | 陈梓瀚(vczh)
@溪流
你可以去看看我的代码哈,判断两个function是否==我做了。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-26 11:18 | guest
GCC可以通过,但VC2008报错:Function0(const T &fun) 说fun是“引用的引用”  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上) 2011-01-26 12:02 | 溪流
@guest
08没试过,就不知道了。不过后来有一个改进(在下篇最后说的),就是 Function<R()> 的第一个构造函数的参数去掉 const &,改成 T fun,同时 FunctionTraits 也不要了。这样可能08下也能过了。  回复  更多评论
  
# re: C++ 下 Function 对象的实现(上)[未登录] 2011-02-21 17:31 | C++爱好者
@OwnWaterloo
完全同意你的观点,其实就让大家按着一种方法写那不是更好吗?为啥还要搞那么多写法,弄得大家都不统一,这不是画蛇添足吗?真不知道那些人是怎么想的。

我顺便也发泄下我对C/C++有些不满意的地方:
关于C++对象的隐式转换,我觉得这个功能只有害处没有好处(如果真要说有好处的话那就是可以少打点字)。比如说:
class Test
{
public:
Test(int a);
};

void func(const Test &t)
{
}

调用1:func(Test(1)); // 手动构造一个Test对象
调用2:func(1); // 自动进行隐式转换

虽然调用2和调用1的操作方式可以说完全一样,但调用2的可读性不强,给人阅读代码带来负担,这又是何必呢?莫非真的是为了省那么几个字母?

还有
void func()
{
}

void func(void)
{
}
这两种写法我更赞同前一种写法。没参数就没参数嘛,为啥非得弄个void在括号里,看着多难看,当然返回值为void另当别论(我承认自己有点主观,但是可以二选一,大家统一一下总可以吧)。  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理