微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

对于特定类型,使用可变参数激发虚函数而不是成员模板函数

如何解决对于特定类型,使用可变参数激发虚函数而不是成员模板函数

我有一个模板成员函数,我想为大多数类型调用它,但对于某些特定类型,我需要调用虚拟成员函数

这样的事情(请不要关注“工厂设计”,它只是为了提出 C++ 问题,而不是因为这是工厂必然需要的设计):

// factory 
class Factory
{
    // default create function
    template<typename T,typename... Args>
    T* create(Args&&...parametes) const
    {
        return new T(std::forward<Args>(parametes)...);
    }

    // Handling creation for type A
    virtual A* create(int i_i,const std::string& i_s) const =0;

    // Handling creation for type C
    virtual C* create(int i_i) const = 0;
};

例如使用:

Factory* pFactory = new DerivedFactoryObject;
A* pA = pFactory->creat<A>(4,"Test");

但它总是调用模板函数而不是 A 或 C 的虚拟函数

我尝试像这样使用模板特化:

    template<>
    template<typename... Args>
    A* create<A>(Args&&...parametes) const
    {
        return createA(std::forward<Args>(parametes)...);
    }

     virtual A* createA(int i_i,const std::string& i_s) const =0;

但实际上无法对函数做偏特化,确实编译失败了。

那我该怎么做?

解决方法

我花了好几天的时间才弄清楚如何去做,所以我很乐意在这里给出我的结论作为教程,以帮助其他人了解我们可能会遇到的细节。它开放供更正和改进,这就是为什么我把它写在这里作为一个问题。

为了展示所涉及的问题,我用一个小例子一步一步地解释了这个主题。在这个例子中,我们希望使用工厂来构建一个对象创建器,我们可以使用不同类型的工厂来获得不同的对象创建方式。

基本版:

首先,让我们从我们工厂的基本版本开始。

// forward decleration
class Creator;

// factory 
class Factory
{
public:
    Factory() = default;
    virtual ~Factory() = default;
private:
    // create is private to prevent using it,only class Creator can use it.
    template<typename T,typename... Args>
    T* create(Args&&...parametes) const
    {
        return new T(std::forward<Args>(parametes)...);
    }

    // allow only calss Creator to use factoy's create function
    friend class Creator;
};

// for having cleaner code
using FactorySPtr = std::shared_ptr<Factory>;

这个工厂可以使用它的模板函数create创建不同类型的对象,它是私有的,只有 Creator 对象可以使用它 - 这就是我们将类 Creator 声明为朋友的原因。

Creator 也很简单:

class Creator
{
public:
    Creator(FactorySPtr  i_spFactory) : m_spFactory(i_spFactory) {}

    template<typename T,typename... Args>
    std::shared_ptr<T> create(Args&&...parametes) const
    {
        T* p = m_spFactory->create<T>(std::forward<Args>(parametes)...);
        return std::shared_ptr<T>(p);
    }   
private:
    FactorySPtr  m_spFactory;
};

在构造时,它存储将用于创建对象的工厂。

为了展示它是如何使用的,假设我们有以下对象 A、B 和 C:

struct A
{
    A(int i_i,const std::string& i_s) : i(i_i),s(i_s) {}
    int i = 0;
    std::string s;
};

struct B
{
    B(const std::vector<float>& i_v) : v(i_v) {}
    std::vector<float> v;
};

struct C
{
    C(int i_i) : i(i_i) {}
    int i = 0;
};

那么我们就可以这样使用creator了:

Creator creator(std::make_shared<Factory>());

std::shared_ptr<A> spA = creator.create<A>(4,"test");
std::shared_ptr<B> spB = creator.create<B>(std::vector<float>({0.1f,0.2f,0.3f}));
std::shared_ptr<C> spC = creator.create<C>(67);

触发虚拟成员函数而不是模板成员函数:

现在假设我们需要工厂允许以不同的方式创建对象 A 和 C。

为此,我们向工厂添加了两个虚函数,当处理的对象是 A 或 C 时,它们将被激发而不是模板创建函数。然后我们可以创建派生工厂类,这些类可以像我们一样以不同的方式处理这些对象愿望。

为简单起见,让我们在 - 替换函数中调用这些虚函数,因为它们打算被激发,而不是针对特定类型的模板。

!! 注意替换函数和模板函数是一样的。

!! 注意替换常量/非常量类型与模板类型相同。在我们的示例中,模板创建函数是一个常量函数,因此我们有使替换函数也为常量。

// factory 
class Factory
{
public:
    Factory() = default;
    virtual ~Factory() = default;

private:
    // create is private to prevent using it,typename... Args>
    T* create(Args&&...parametes) const
    {
        return new T(std::forward<Args>(parametes)...);
    }

    // function replacement for A
    virtual A* create(int i_i,const std::string& i_s) const
    {
        return new A(i_i,i_s);
    }
    
    // function replacement for C
    virtual C* create(int i_i) const
    {
        return new C(i_i);
    }

    // allow only calss Creator to use factoy's create function
    friend class Creator;
};
using FactorySPtr = std::shared_ptr<Factory>;

让我们创建一个在创建 A 和 C 时进行不同处理的派生工厂

// eve factory 
class FactoryEve : public Factory
{
public:
    FactoryEve() = default;
    virtual ~FactoryEve() = default;

private:
    virtual A* create(int i_i,const std::string& i_s) const
    {
        A* p = new A(i_i,i_s);
        p->s += "_Hey!";
        return p;
    }

    virtual C* create(int i_i) const
    {
        C* p = new C(i_i);
        p->i += 5;
        return p;
    }
};

但这行不通!

让我们检查一下。

在接下来的运行中,我们构造了一个 FactoryEve 类型的创建器,以便在创建 A 和 C 时得到不同的处理。

跟踪调用表明所有创建都是使用模板 Factory::create 函数完成的,而不是替换函数。

Creator creator(std::make_shared<FactoryEve>());

std::shared_ptr<A> spA = creator.create<A>(4,"test");                              // Bug -- created by template<typename T> T* Factory::create
std::shared_ptr<B> spB = creator.create<B>(std::vector<float>({0.1f,0.3f}));   // Ok  -- created by template<typename T> T* Factory::create
std::shared_ptr<C> spC = creator.create<C>(67);                                     // Bug -- created by template<typename T> T* Factory::create

为什么会这样?

因为我们实际上只调用模板表单 - 请参阅包含调用行的 Creator::create 函数:

T* p = m_spFactory->create<T>(std::forward<Args>(parametes)...);

我们使用模板参数调用 create ,这意味着 - 仅调用模板函数,编译器完全按照我们的要求执行。

因此,为了让编译器找到非模板函数的匹配项,我们必须将行更改为。

T* p = m_spFactory->create(std::forward<Args>(parametes)...);

这样,编译器会为类型 T 选择最佳匹配。如果该类型有显式函数,它会优先选择它,否则,它将使用模板。

!! 当调用模板函数时,可能有非模板替换函数,或者不同的模板参数函数,调用时不要带模板参数。

但是现在,如果您删除模板参数,则会出现编译器错误:(

为什么会这样?

因为编译器无法根据我们函数的形式返回类型找到正确的函数。

!! 不能根据返回类型进行替换。

当我们使用模板参数时,编译器确切地知道要采用什么函数,但是我们需要在没有模板参数的情况下调用 create 以允许由非模板函数替换。

因此,我们必须改变我们的函数形式,在函数参数中包含对象类型。

这里是Factory对象的固定代码,派生类FactoryEve也要相应的固定。

// factory 
class Factory
{
public:
    Factory() = default;
    virtual ~Factory() = default;

private:
    // create is private to prevent using it,only class Creator can use it.
    // return object pointer is placed as a parameter to allow the compiler to find the correct function
    
    template<typename T,typename... Args>
    void create(T*& o_p,Args&&...parametes) const
    {
        o_p = new T(std::forward<Args>(parametes)...);
    }

    // function replacement for A
    virtual void create(A*& o_p,int i_i,const std::string& i_s) const
    {
        o_p = new A(i_i,i_s);
    }

    // function replacement for C
    virtual void  create(C*& o_p,int i_i) const
    {
        o_p = new C(i_i);
    }

    // allow only calss Creator to use factoy's create function
    friend class Creator;
};

对象创建者也应该被修复。

class Creator
{
public:
    Creator(FactorySPtr  i_spFactory) : m_spFactory(i_spFactory) {}

    template<typename T,typename... Args>
    std::shared_ptr<T> create(Args&&...parametes) const
    {
        T* p;
        m_spFactory->create(p,std::forward<Args>(parametes)...);
        return std::shared_ptr<T>(p);
    }   
private:
    FactorySPtr  m_spFactory;
};

好的,如果我们现在运行它,我们会得到改进,但是,它不会按需要做所有事情。

跟踪调用表明,对于对象 C,它按预期为 C 使用 FactoryEve::create,但对于对象 A,它仍然使用 Factory 基类的模板函数。

Creator creator(std::make_shared<FactoryEve>());

std::shared_ptr<A> spA = creator.create<A>(4,"test");                              // Bug -- created by template<typename T> Factory::create 
std::shared_ptr<B> spB = creator.create<B>(std::vector<float>({0.1f,0.3f}));   // Ok  -- created by template<typename T> Factory::create
std::shared_ptr<C> spC = creator.create<C>(67);                                     // Ok  -- created by FactoryEve::create(C*& o_p,int i_i)

为什么会这样?

因为我们为创建 A 提供的输入参数(4,“test”)的参数包类型被视为:

int,const char[5]

因此,在对象 A 的情况下,对于行 m_spFactory->create(p,std::forward(parametes)...);编译器搜索具有

形式的函数

void Factory::create(A*&,int&&,const char[5]&) const

但是我们为 A 声明的虚函数具有不同的形式并且使用 std::string 而不是 char[5]

void create(A*& o_p,const std::string& i_s) const

这就是为什么它不适用于 A,而仅适用于 C。

问题是编译器在选择匹配函数时,并没有考虑转换。

那我们能做什么?我们不想强制应用程序使用 std::string 而是让它保持友好,我们不想为每个可能的转换类型编写虚函数,因为它使我们的代码与许多函数一起煮沸,而且我们肯定会忘记一些打字……看起来真的很吓人!

幸运的是,我们有一个解决方案。让我们使用参数包本身作为我们替换函数的输入参数。通过这种方式,它可以确保编译器从应用程序调用中获得的任何类型,我们的函数都将具有相同的形式!

!! 替换带参数包的函数时,最好也使用带参数包的函数。

原则上,我们想做这样的事情

    // function replacement for A
    template<typename... Args>
    virtual void create(A*& o_p,Args&&...parametes) const
    {
        o_p = new A(i_i,i_s);
    }

但是我们不能,因为虚函数不能是模板!

哈!似乎我们只是从一个问题走向一个问题。

好吧,幸运的是我们可以通过使用调用虚函数的模板替换函数轻松解决它,如下所示:

(请注意,虚函数现在有一个唯一的名称 createA 和 create C)

// factory 
class Factory
{
public:
    Factory() = default;
    virtual ~Factory() = default;

private:
    // create is private to prevent using it,Args&&...parametes) const
    {
        o_p = new T(std::forward<Args>(parametes)...);
    }

    // function replacement for A
    template<typename... Args>
    void create(A*& o_p,Args&&...parametes) const
    {
        // calling virtual function which create A 
        createA(o_p,std::forward<Args>(parametes)...);
    }

    // function replacement for C
    template<typename... Args>
    void create(C*& o_p,Args&&...parametes) const
    {
        // calling virtual function which create C 
        createC(o_p,std::forward<Args>(parametes)...);
    }

    virtual void createA(A*& o_p,i_s);
    }

    virtual void  createC(C*& o_p,int i_i) const
    {
        o_p = new C(i_i);
    }

    // allow only calss Creator to use factoy's create function
    friend class Creator;
};

通过这种方式,我们在使用相同的参数包的同时替换了通用的创建模板函数,以确保我们获得相同的表单,并且仍然有虚拟机制允许不同的工厂类型用于不同的创建方法。

现在它起作用了!

不要忘记将FactoryEve中的函数名称也修改为createA和createC,现在如果你运行,你会得到你想要的:

Creator creator(std::make_shared<FactoryEve>());

std::shared_ptr<A> spA = creator.create<A>(4,"test");                              // Ok  -- created by FactoryEve::createA
std::shared_ptr<B> spB = creator.create<B>(std::vector<float>({0.1f,0.3f}));   // Ok  -- created by template<typename T> Factory::create
std::shared_ptr<C> spC = creator.create<C>(67);                                     // Ok  -- created by FactoryEve::createC

最后的话:

对于使用虚函数替换带有参数包的模板成员函数,我们实际上不需要将它们用作替换而是从不同的模板替换函数中调用它们。

  • 注意替换函数将具有相同的 表单作为模板之一 - 还要注意常量/非常量 函数类型。
  • 调用模板函数时,可能有非模板 替换函数,或不同的模板参数函数,在没有模板参数的情况下调用它。
  • 不能根据返回类型进行替换。因此,如果您的类型 是唯一标识该函数的函数,将其作为参数放置。
  • 使用参数包替换函数时,最好使用具有相同参数包的替换函数。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。