65.9K
CodeProject 正在变化。 阅读更多。
Home

可变参数委托

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2015年4月13日

CPOL

8分钟阅读

viewsIcon

22394

downloadIcon

305

本教程展示了如何实现 C++ 代理,这些代理能够绑定到具有任意签名的方法和函数,即任意数量和类型的参数以及返回值。

引言

有一段时间,我需要为我的框架编写一个创建图形效果的子系统。许多渲染效果的一个特点是执行一组操作,在着色器部分生效之前和之后改变管道状态。换句话说,我们需要一种“运算符括号”—— Begin()/End() 函数——在其中实现效果算法。我没有找到现成的解决方案,于是自己写了一个实现。

需要将函数代理和方法代理保存在同一个容器中。被代理实体的签名应该是任意的。必须具备在实际调用之前绑定被代理方法和函数的能力,从而——立即调用具有预先绑定参数的整个代理集合的能力。参数应该通过完美转发传递和保存,以避免不必要的副本。在创建代理时,用户不应该编写多余的代码,表示法必须符合常识,模板参数中没有签名和数据类型。代理应该是这样使用的。

// Constructing delegates' collection
DelegatesCollection dc;
// Adding delegates for functions f1, f2 and for methods meth1, meth2
dc.add(&f1);
dc.add(&obj1, &meth1);
dc.add(&obj2, &meth2);
dc.add(&f2);
// Calling f1 through its delegate and passing in arguments
dc[0](arg1, arg2, . . ., argN);
// Binding arguments to meth1's delegate
dc[1].bind_args(arg1, arg2, . . ., argN);
// Calling meth1 with previously bound arguments
dc[1].call_with_bound_args();
// Binding arguments to meth2 and f2
dc[2].bind_args(arg1, arg2, . . ., argN);
dc[3].bind_args(arg1, arg2, . . ., argN);
// Batch calling entire delegates' collection, 
// i.e. f1, meth1, meth2 and f2, with previously bound arguments
dc.batch_call_with_bound_args();

总而言之,所有设想的东西都实现了。

特点

  1. 能够将函数代理和方法代理保存在同一个容器中
  2. 能够使用绑定的参数批量调用整个代理集合
  3. 代理存储在容器 std::vector
  4. 参数和结果数据类型是从函数签名自动推导出来的——无需将其指定为模板参数

代理设计

代理的设计是为了解决以下任务:

  1. 为了存储方法代理 (MD) 和函数代理 (FD),两者都必须用一个接口来描述。
  2. 在输出中,我们应该得到一个 Delegate 类,其中包含代理概念的实现。
  3. 用户应该能够创建代理集合。这将需要 DelegatesSystem 类,它将作为 Delegate 实例的容器。
  4. DelegatesSystem 必须提供批量调用具有绑定参数的代理函数和方法的接口,即顺序启动所有代理集合元素。

创建 MD 和 FD 的接口

由于在一般情况下,我们将代理具有任意参数数量的方法和函数,因此创建的接口必须是模板。对于 MD 创建,必须存在对象指针及其方法;对于 FD,则需要函数指针。这些指针将通过模板参数传递。

Delegate 类必须独立于具体数据类型,因为它是一个通用的代理概念抽象,因此它不能是模板。

问题随之而来,我们如何用一个接口描述 MD 和 FD,并将它们与 Delegate 类关联起来?

唯一的方法是创建一个对 MD 和 FD 通用的接口,并存储初始化了适当的实现指针的其实例,存储在 Delegate 内部。

下一个问题是如何同时为 MD 和 FD 获得模板接口来描述这些实体?

幸运的是,存在模板特化(partial template specialization),我们将用它来解决这个任务。

基本上 MD 和 FD 是同一件事,它们只在一个组件上有所不同。因此,描述它们的接口将有两个特化。

实现

通用接口

class IDelegateData abstract { needed pure virtual methods are here };

通用模板

template<class...Args> class DelegateData : public IDelegateData {};

由于方法与函数不同,前者在一个类的具体对象上调用,因此通用模板对 MD 的特化还将包含一个参数,用于指定方法所属的类的类型:class O

因此,我们将得到通用模板接口的以下两个特化:

用于方法

template<class R, class O, class... Args>
class DelegateData<R, O, R(Args...)> : public IDelegateData
{
public:

typedef R(O::*M)(Args...);

DelegateData(O* pObj, M pMethod) : m_pObj(pObj), m_pMethod(pMethod) {}
private:

    O* m_pObj;
    M  m_pMethod;
};

用于函数

template<class R, class... Args>
class DelegateData<R, R(*)(Args...)> : public IDelegateData
{
public:

typedef R(*F)(Args...);

DelegateData(F pF) : m_pF(pF) {}
private:

    F m_pF;
};

委托

Delegate 中存储 DelegateData 实例

class Delegate
{
public:

Delegate() = default;

// For methods

template<class R, class O, class...Args>
explicit Delegate(O* pObj, R(O::*M)(Args...))
{
    bind(pObj, M);
}

template<class R, class O, class...Args>
void bind(O* pObj, R(O::*M)(Args...))
{
    m_data = new DelegateData<R, O, R(Args...)>(pObj, M);
}

// For functions
template<class R, class...Args>
explicit Delegate(R(*F)(Args...))
{
    bind(F);
}

template<class R, class...Args>
void bind(R(*F)(Args...))
{
    m_data = new DelegateData<R, R(*)(Args...)>(F);
}

private:

    IDelegateData* m_data;

};

Delegate::m_data 字段将根据调用哪个 Delegate 构造函数,相应地填充 MD 或 FD 的 DelegateData 特化。

DelegatesSystem

让我们考虑 DelegatesSystem 类,它将以 Delegate 对象的形式存储代理集合。

class DelegatesSystem
{
private:
    vector<Delegate> m_delegates;
};

剩下的就是“教会” DelegatesSystem 将代理 Delegate 添加到集合中,并通过索引访问它们。

将代理添加到集合中最简单的方法是直接在添加方法中列出 Delegate 构造函数所需的参数。为此,我们将需要可变参数模板、rvalue 引用以及新的顺序容器方法 emplace_back(...),它接受参数包并通过在原地构造对象(in place construction)将新元素添加到集合中,调用其类中与传递的参数相对应的构造函数。

class DelegatesSystem
{
public:

template<class...Args>
void add(Args&&... delegateCtorArgs)
{
    m_delegates.emplace_back(std::forward<Args>(delegateCtorArgs)...);
}

Delegate& operator[](uint idx)
{
    return delegates[idx];
}

private:

    vector<Delegate> m_delegates;
};

现在,我们可以通过调用 Delegate 类的任何构造函数来添加代理,所有这些都通过一个方法完成。

参数

我们离目标不远了。剩下的唯一事情是提供调用具有任意数量参数的代理的可能性。因此,我们必须先对目前为空的接口进行更改。让我们向 IDelegateData 添加一个纯虚方法来调用代理。

class IDelegateData abstract { public: virtual void call(void*) abstract; };

我们将在 DelegateData::call 方法中接收参数,将它们放置在 Arguments 类的实例中传递给它,然后从中提取它们,将 void* 转换为 Arguments<...>*

具体来说,Arguments 将存储在 Arguments 内部的元组中,因为这是一个允许存储任意数量不同类型元素的容器。

template<class... Args>
class Arguments
{
public:

    Arguments(Args&&... args) : m_args(std::forward_as_tuple(std::forward<Args>(args)...)) {}

public:

    std::tuple<Args&&...> m_args;
};

在传递参数时,我们使用完美转发,以便为 rvalues 绘制移动语义,并为 lvalues 按引用传递。

我们有以下调用链,最终调用被代理的方法/函数:

Delegate::operator() -> DelegateData::call

我们重载了 Delegate::operator() 运算符,使用了可变参数模板,使其能够接收任意数量的参数。

class Delegate
{
public:

previous declarations

template<class...Args>
void operator()(Args&&... args)
{
    m_data->call(new Arguments<Args...>(std::forward<Args>(args)...));
}

private:

    IDelegateData* m_data;
};

在这里,我们创建了 Arguments 的实例,将 args 参数包存储在其中,并将其传递给多态字段 m_datacall() 方法,该方法触发 DelegateData 的相应特化的 call(),其中参数被以某种方式提取并传递以调用被代理的函数/方法。

template<class R, class O, class... Args>
class DelegateData<R, O, R(Args...)> : public IDelegateData
{
public:

previous declarations

void call(void* pArgs) override
{
   extraction of arguments from pArgs and passing them to the delegated function/method;
   i.e. in the high-level sense the following will occur:

(m_pObj->*m_pMethod)(extraction of arguments pack from pArgs);
}

private:

    O* m_pObj;
    M  m_pMethod;

};

对于实现 FD 概念的 DelegateData 特化,一切看起来都类似。

从元组中提取参数并传递给代理

现在出现了从元组中提取参数并将其传递给方法或函数的任务。目前,有一个标准的提取元组元素的方法,为此我们在 header <tuple> 中有一个模板函数 std::get<idx>(tuple),它将所需元素的索引作为模板参数 idx。我们有参数包 Args(在 DelegateData 的特化中定义),它包含被代理函数/方法的参数的数据类型,因此我们知道如何从 void* 转换。我们希望如下解决此任务(我们将使用 MD 的示例,并且不使用完美转发):

(m_pObj->*m_pMethod)(std::get<what_goes_here>(static_cast<Arguments<Args...>*>(pArgs)->args)...);

也就是说,我们需要参数包 Args 来将 pArgs 转换为相应的 Arguments 指针。调用 get() 函数应该通过索引包展开来实现,这将允许将存储在元组中的参数作为单个列表提取并传递给被代理的方法/函数。因此,现在我们必须找到一种生成与 Args 包中的参数数量相对应的索引列表的方法。有一种方法,它基于元编程和模板递归 [IndicesTrick]。下面是索引创建机制的样子:

template<int... Idcs> class Indices{};

template<int N, int... Idcs> struct IndicesBuilder : IndicesBuilder<N - 1, N - 1, Idcs...> {};

template<int... Idcs>
struct IndicesBuilder<0, Idcs...>
{
    typedef Indices<Idcs...> indices;
};

为了加快速度,我将为不熟悉元编程和模板递归的人提供这项技术操作原理的简要高层解释 [Metaprog]。IndicesBuilder 模板应该被视为一个函数 f(N),它接受一个整数参数,表示索引的数量,并返回这些索引的包。为了计算索引的数量,我们将利用新的函数 sizeof...(parameter_pack),将其应用于 Args:  sizeof...(Args)

我们将通过另一个方法来传递索引,在该方法中我们将调用被代理的方法。

template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    (m_pObj->*m_pMethod)(std::get<Idcs>(static_cast<Arguments<Args...>*>(pArgs)->args)...);
}

添加完美转发并简化表达式后:

template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    auto pArguments = static_cast<Arguments<Args...>*>(pArgs);
    (m_pObj->*m_pMethod)(std::get<Idcs>(pArguments->m_args)...);
}

重载方法调用的最终实现将如下所示:

void call(void* pArgs) override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), pArgs);
}

DelegateData 模板的 MD 特化(完整版):

template<class R, class O, class... Args>
class DelegateData<R, O, R(Args...)> : public IDelegateData
{
public:

typedef R(O::*M)(Args...);

    DelegateData(O* pObj, M pMethod) : m_pObj(pObj), m_pMethod(pMethod) {}

void call(void* pArgs) override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), pArgs);
}

template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    auto pArguments = static_cast<Arguments<Args...>*>(pArgs);
    (m_pObj->*m_pMethod)(std::get<Idcs>(pArguments->m_args)...);
}

private:

    O* m_pObj;
    M  m_pMethod;
};

FD 的特化也将类似。

这样,就解决了存储任意数量的函数和方法代理(具有任何给定签名)并使其能够以后调用的任务。剩下的就是添加参数绑定和代理的批量调用。

参数绑定和代理的批量调用

为了能够批量调用代理,我们将不得不将参数绑定到它们。相应地,每个代理应该能够存储其参数集。并且在代理批量调用时,与其绑定的参数将被传递给相应的方法/函数。

IDelegateData

让我们向绑定参数到代理的接口添加 bind_args,以及调用带有绑定参数的代理的接口 - call_with_bound_args,以及存储实际参数的字段 void* m_pBound_args

class IDelegateData abstract
{
public:
    virtual void call(void*) abstract;
    virtual void call_with_bound_args() abstract;
    virtual void bind_args(void*) abstract;
protected:
    void* m_pBound_args;
};

DelegateData

为了存储参数,让我们在 DelegateData 的每个特化中添加构造函数重载,并重载 bind_argscall_with_bound_args

// Data for methods
template<class R, class O, class... Args>
class DelegateData<R, O, R(Args...)> : public IDelegateData
{
public:

typedef R(O::*M)(Args...);

DelegateData(O* pObj, M pMethod) : m_pObj(pObj), m_pMethod(pMethod) {}

void call(void* pArgs) override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), pArgs);
}

template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    auto pArguments = static_cast<Arguments<Args...>*>(pArgs);
    (m_pObj->*m_pMethod)(std::get<Idcs>(pArguments->m_args)...);
}

public:

DelegateData(O* pObj, M pMethod, Args&&... argsToBind) : m_pObj(pObj), m_pMethod(pMethod)
{
    bind_args(new Arguments<Args&&...>(std::forward<Args>(argsToBind)...));
}

virtual void bind_args(void* argsToBind) override
{
    if (argsToBind != m_pBound_args)
    {
        delete m_pBound_args;
        m_pBound_args = argsToBind;
    }
}

void call_with_bound_args() override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), m_pBound_args);
}

private:

    O* m_pObj;
    M  m_pMethod;
};

// Data for functions
template<class R, class... Args>
class DelegateData<R, R(*)(Args...)> : public IDelegateData
{
public:

typedef R(*F)(Args...);

DelegateData(F pF) : m_pF(pF) {}

void call(void* pArgs) override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), pArgs);
}

template<int...Idcs>
void invoker(Indices<Idcs...>, void* pArgs)
{
    auto pArguments = static_cast<Arguments<Args...>*>(pArgs);
    m_pF(std::get<Idcs>(pArguments->m_args)...);
}

public:

DelegateData(F pF, Args&&... argsToBind) : m_pF(pF)
{
    bind_args(new Arguments<Args&&...>(std::forward<Args>(argsToBind)...));
}

virtual void bind_args(void* argsToBind) override
{
    if (argsToBind != m_pBound_args)
    {
        delete m_pBound_args;
        m_pBound_args = argsToBind;
    }
}

void call_with_bound_args() override
{
    invoker(typename IndicesBuilder<sizeof...(Args)>::indices(), m_pBound_args);

}

private:

    F m_pF;
};

委托

对于 Delegate,我们将添加构造函数和方法的重载:bind,模板方法 bind_argscall_with_bound_args,它们仅作为 IDelegateData 接口的包装器。

class Delegate
{
public:
...

template<class R, class O, class...Args, class...ArgsToBind>
explicit Delegate(O* m_pObj, R(O::*M)(Args...), ArgsToBind&&... argsToBind)
{
    bind(m_pObj, M, std::forward<ArgsToBind>(argsToBind)...);
}
template<class R, class...Args, class...ArgsToBind>
explicit Delegate(R(*F)(Args...), ArgsToBind&&... argsToBind)
{
    bind(F, std::forward<ArgsToBind>(argsToBind)...);
}
template<class R, class O, class...Args, class...ArgsToBind>
void bind(O* pObj, R(O::*M)(Args...), ArgsToBind&&... argsToBind)
{
    m_data = new DelegateData<R, O, R(Args...)>(pObj, M, std::forward<ArgsToBind>(argsToBind)...);
}
template<class R, class...Args, class...ArgsToBind>
void bind(R(*F)(Args...), ArgsToBind&&... args)
{
    m_data = new DelegateData<R, R(*)(Args...)>(F, std::forward<ArgsToBind>(args)...);
}
template<class... Args>
void bind_args(Args&&... args)
{
    m_data->bind_args(new Arguments<Args...>(std::forward<Args>(args)...));
}

void call_with_bound_args()
{
    m_data->call_with_bound_args();
}
...
};

DelegatesSystem

对于 DelegatesSystem,我们将添加启动批量调用整个代理集合的功能。

class DelegatesSystem
{
public:
...
void launch()
{
    for (auto& d : m_delegates)
        d.call_with_bound_args();
}
private:
    vector<Delegate> m_delegates;
};

示例代码

本教程的配套代码是使用 Visual Studio 2013 Community Edition 编写的。

参考文献

© . All rights reserved.