首页 > 解决方案 > 对于特定类型,使用可变参数触发虚函数而不是成员模板函数

问题描述

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

像这样的东西(请不要专注于“工厂设计”,它只是为了提出 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;

但实际上对函数做部分特化是不可能的,而且确实编译失败了。

那么我该怎么做呢?

标签: c++11templatesvariadic-templates

解决方案


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

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

基础版:

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

// 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(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 时,它们将被触发而不是模板创建函数。然后我们可以创建派生工厂类,它们可以按照我们的意愿以不同的方式处理这些对象。

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

!! 注意替换功能与模板的形式相同。

!! 注意替换 const/non const 类型与模板类型相同。在我们的示例中,模板创建函数是一个 const 函数,因此我们也必须将替换函数设为 const。

// 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)...);
    }

    // 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.2f,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;
};

对象 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.2f,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, int i_i, 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, 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
    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, int i_i, const std::string& i_s) const
    {
        o_p = new A(i_i, 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.2f,0.3f}));   // Ok  -- created by template<typename T> Factory::create
std::shared_ptr<C> spC = creator.create<C>(67);                                     // Ok  -- created by FactoryEve::createC

最后的话:

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

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

推荐阅读