c++11 - 对于特定类型,使用可变参数触发虚函数而不是成员模板函数
问题描述
我有一个模板成员函数,我想为大多数类型调用它,但对于某些特定类型,我需要调用虚拟成员函数。
像这样的东西(请不要专注于“工厂设计”,它只是为了提出 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(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 函数类型。
- 当调用可能具有非模板替换函数或不同模板参数函数的模板函数时,请在没有模板参数的情况下调用它。
- 无法根据返回类型进行替换。因此,如果您的类型是唯一标识函数的类型,请将其作为参数放置。
- 当用参数包替换函数时,最好使用具有相同参数包的替换函数。
推荐阅读
- javascript - 如何通过在javascript中调用函数在对象内添加属性
- javascript - 如果选中其他单选按钮,请检查单选按钮
- html - 类型号不适用于小数位
- css - 在我的情况下,我必须为一行使用 5 col-md,另一个 col-md 需要在没有打开行的情况下进入下一行
- python - 如何将 dict keyx:valuex 列表转换为 dict key:keyx, value:valuex 列表?
- vue-material - 未在 Vue Material md-tabs 中列出的页面会抛出“未捕获的类型错误:无法读取属性 'parentNode' of null”
- matplotlib - 我可以反转matplotlib的pcolormesh的过程吗?
- mysql - MYSQL JOIN 3 tables where field in third tables must be 0 and nothing else
- mysql - Heroku deployment hangs on executing DB queries, then fails with Timed out running buildpack Node.js
- regex - 正则表达式获取列