在派生类中使用带有虚假参数的类模板。






3.70/5 (7投票s)
2007 年 8 月 22 日
9分钟阅读

52771

136
一篇文章提供了新的 C++ 惯用法来避免代码重复。
引言
模板和继承的结合为在 C++ 中创建强大的技术开辟了新的途径 [1-3]。本文介绍了一种避免在某些特殊情况下代码重复的新技术,并提供了说明性示例。该技术非常简单,令人惊讶的是它尚未发表。
动机
这项技术的主要动机是提供一种避免代码重复的新方法。
定义
我们将遵循标准的模板术语。有关此术语的参考资料可以在 [4,§45] 中找到。
请注意 类模板 和 模板类 之间的区别
类模板 是用于生成模板类的模板。您不能声明类模板的对象。
模板类 是类模板的一个实例。
起点
考虑以下简单的构造
template <class T>
class Derived : public Base (1)
类模板 Derived
可以包含任何成员函数、成员、静态数据成员等,但类的成员完全不依赖于模板参数 T
。因此,从某种意义上说,T
是一个虚假参数,可以简单地省略。
template <class>
class Derived : public Base (1-a)
对于类 Derived
的任何特化,我们需要提供一个特定的参数 T
。参数本身不发挥任何作用;它仅用于特化,在这种情况下它将只是一个空结构。
struct Derived1_Tag (2)
{};
类型名称反映了虚假参数类似于标签。接下来的(可选)步骤是提供一些命名约定来连接标签和模板特化。我们将通过 typedef 来关联标签名和模板类的名称。
typedef Derived<Derived1_Tag> Derived1; (3)
因此,模板参数根据约定包含了派生模板类的名称。在本文的其余部分,我们将该惯用法 (1-3) 命名为TFP 惯用法(带有虚假参数的模板)。
技术就到此为止。它非常简单,但接下来我们将看到何时以及如何使用它。
使用该技术
该技术可用于以下情况:
- 一组除类名外完全相同的派生类。
- 一组在行为方面略有不同的派生类。
让我们看第二种情况。假设我们有一个基类
class Base
{
//……
virtual void foo1() =0;
virtual void foo2() =0;
//…..
};
以及 3 个派生类
class Derived1 : public Base
{
//…….
virtual void foo1();
virtual void foo2();
//…….
};
class Derived2 : public Base
{
//…….
virtual void foo1(); //implementation is the same as in Derived1
virtual void foo2();
//…….
};
class Derived3 : public Base
{
//…….
virtual void foo1();
virtual void foo2(); // implementation is the same as in Derived1
//…….
};
其他细节省略,但假设派生类相同,只是它们具有不同的函数 foo1() 和 foo2() 的实现。
在这种情况下,我们按照以下步骤操作:
步骤 1:用带有虚假参数的类模板替换派生类。函数foo1() 和foo2() 的默认实现采用类 Derived1 的实现。
template<class>
class Derived : public Base
{
//…….
virtual void foo1(); //class Derived1 implementation
virtual void foo2(); // class Derived1 implementation
//…….
};
步骤 2:为所有派生类定义空结构。
struct Derived1_Tag
{};
struct Derived2_Tag
{};
struct Derived3_Tag
{};
步骤 3:(可选,但有用)使用 typedef 为类提供用户友好的名称。
typedef Derived< Derived1_Tag> Derived1;
typedef Derived< Derived2_Tag> Derived2;
typedef Derived< Derived3_Tag> Derived3;
步骤 4:特化类之间不同的派生函数。
template <>
void Derived2::foo1()
{
//…implementation
}
template <>
void Derived3::foo2()
{
//…implementation
}
有趣的是,成员函数的特化起着与覆盖相同的作用。
我必须强调,特化单个成员函数而不特化所有成员函数是合法的。
根据 C++ 标准 [5,§14.7.3]:
"类模板的成员函数、成员类或静态数据成员可以为隐式实例化的类特化而显式特化。"
如果您在模板术语,如“显式实例化”、“隐式实例化”、“部分特化”等方面遇到问题,请查阅 [4,§45-48]。如果您不想深入研究,只需记住标准允许您特化部分成员函数而不特化所有成员函数。
让我们用更多示例来说明这项技术。
替换宏
作为第一个示例,我们将考虑一个 Exception 类。这个想法来自 Xerces 库 [6],并且为了说明起见,该类已被简化。
让我们观察库中如何实现异常。它们提供了一个像这样的基类(Xerces Exception 的实际实现不同)。
class BaseException
{
public:
BaseException();
BaseException(const std::string &what);
virtual ~BaseException() = 0;
BaseException(const BaseException& e);
BaseException& operator=(const BaseException& e);
virtual const char* what() const;
private:
std::string what_;
};
所有其他异常类都相同,除了异常的名称。由于反复输入它们很麻烦,Xerces 作者用宏替换了它们。
#define MakeDerivedException(DerivedException) \
class DerivedException : public BaseException \
{ \
public: \
\
DerivedException() : BaseException(){ } \
\
explicit DerivedException( const char* what) : \
BaseException(what){ } \
\
explicit DerivedException(const std::string &what) : \
BaseException(what) { } \
\
virtual ~DerivedException() {}; \
\
DerivedException(const DerivedException& e) : \
BaseException(e) { } \
\
DerivedException& operator=(const DerivedException& e) \
{ \
BaseException::operator=(e); \
return *this; \
} \
};
使用宏很容易定义任何客户端异常,例如:
MakeDerivedException (XSerializationException)
让我们应用TFP 惯用法。
基类不变。宏将被类模板替换。
template <class>
class DerivedException : public BaseException
{
public:
DerivedException() : BaseException(){ }
explicit DerivedException( const char* what) :
BaseException(what){ }
explicit DerivedException(const std::string &what) :
BaseException(what) { }
virtual ~DerivedException() {};
DerivedException(const DerivedException& e) :
BaseException(e) { }
DerivedException& operator=(const DerivedException& e)
{
BaseException::operator=(e);
return *this;
}
};
我们省略模板参数,因为它不影响 DerivedException
的定义。
现在您可以通过特化 DerivedException
来定义客户端异常。
struct XSerializationException_Tag{}
typedef DerivedException< XSerializationException_Tag> XSerializationException;
为两种情况都提供了示例代码(请参阅演示项目MacroException 和FakeTempleteException)。
让我们尝试弄清楚哪个是更好的解决方案。从使用的角度来看,它们是等效的。它们都允许我们避免冗余。TFP 惯用法允许您的客户端从 XSerializationException 类继承以满足他们的需求。您也可以从宏定义派生。
我们可以看看TFP 惯用法解决方案与宏解决方案的“利”与“弊”。
弊
- 使用宏,您只需一个语句即可定义派生类,而使用 TFP 惯用法,您需要两个语句才能达到相同效果。
Pro
- “关于宏的第一个规则是:除非必须,否则不要使用它们。” [7,§7.8]。您可以在 C++ 书籍 [3,§2]、[8,§16] 中找到为什么通常最好避免使用宏。
- 从实际角度来看,TFP 惯用法允许您使用编译器调试代码,而宏解决方案则不能。
在 Clone 类中使用
Clone 类出现在类层次结构中,其中有一个成员函数会创建派生对象的精确副本。它有时在 C++ 中被称为“虚拟构造函数”[4],或者更普遍地称为原型模式 [9]。
考虑以下原型图
基类抽象类 IDataReader
代表一个原型,它声明了一个克隆自身的接口。子类实现克隆自身的操作,客户端通过要求原型克隆自身来创建 DerivedReader
。
假设我们的 Reader 从不同的源获取数据。源可以是具有不同格式的文件或数据库中的数据。因此,特定的 Reader 有一个 read 函数,它会填充某些容器,例如字符串向量。
根据特定数据,Reader 可以返回 true
或 fa
lse
。例如,如果我们检查表中某个记录的唯一性。那么,我们选择数据,如果我们有多个 ID 相同的记录,Reader 应该返回 false
。或者,我们需要通过某个 ID 从表中检索特定记录。如果该记录确实存在,我们返回 true
,如果不存在,我们返回 false
。
总的来说,我们的 Reader 非常相似,只是 read 函数的实现不同。让我们回到并为类提供一些实现。
根据 [8,§54] 中的示例,我们来看基类的示例代码
class IDataReader
{
public:
typedef std::auto_ptr IDataReaderPtr;
typedef std::vector<:string> VecString;
IDataReader();
virtual ~IDataReader() = 0;
IDataReaderPtr clone() const
{
doClone();
IDataReaderPtr p = doClone();
assert(typeid(*p)==typeid(*this)&&"doClone incorrectly overriden");
return p;
}
bool read(VecString& outs)
{
return doRead(outs);
}
protected:
IDataReader(const IDataReader& reader);
virtual IDataReaderPtr doClone() const = 0;
virtual bool doRead( VecString& outs) = 0;
private:
IDataReader& operator=(const IDataReader&);
};
让我们看一下实现的一些细节。
首先,使用了非虚拟接口(NVI)惯用法[3,8] 进行设计。基类通过非虚拟函数clone() 和read() 定义接口,这些函数调用受保护的虚拟函数对应项doClone() 和doRead()。使用NVI的一个优点是它允许我们在clone() 函数中包含类型检查。它会提醒我们,如果进一步派生的类没有实现doClone() 函数,或者该函数没有返回IDataReader 类型的对象。
接下来,我们不从clone() 函数返回指针,而是返回auto_ptr(遵循“源”惯用法 [10,§37])。其优点是它是一种让调用者了解指针所有权的安全方式。即使调用者忽略返回值,已分配的对象也将始终被安全删除。
现在,让我们开始实现我们的派生类。我们需要为任何派生类提供构造函数、析构函数、复制构造函数、doClone() 和doRead() 函数,并明确禁止赋值运算符。例如,对于我们的第一个派生类
class DerivedReader1 : public IDataReader
{
typedef IDataReader::IDataReaderPtr IDataReaderPtr;
public:
DerivedReader1() : IDataReader() {}
virtual ~DerivedReader1() {}
protected:
DerivedReader1(const DerivedReader1& rhs) :
IDataReader(rhs) { }
virtual IDataReaderPtr doClone() const
{
IDataReaderPtr ptr(new DerivedReader(*this));
return ptr;
}
virtual bool doRead( VecString& outs);
private:
DerivedReader& operator=(const DerivedReader&);
};
一旦我们开始编写下一个派生类,我们就会发现我们一遍又一遍地重复相同的代码。一个派生类的代码与其他派生类不同的地方仅在于doRead() 函数。其他代码是冗余的。
因此,遵循 TFP 惯用法,我们覆盖代码
template <class>
class DerivedReader : public IDataReader
{
// exactly the same code as in a class Derived1
};
创建标签
struct DerivedReader1_Tag{};
struct DerivedReader2_Tag{};
并特化类
typedef DerivedReader DerivedReader1;
typedef DerivedReader DerivedReader2;
因此,我们可以专注于实现派生类的doRead() 函数,而不是复制代码。例如:
template <>
bool Derived1Reader::doRead( VecString& outs)
{
//read from file of one format
std::cout << "Call Derived1Reader::doRead" << std::endl;
outs.push_back("dummy_data1");
//return error if doesn't get some data
return !outs.empty();
}
template <>
bool Derived2Reader::doRead( VecString& outs)
{
//read from file of another format
std::cout << "Call Derived2Reader::doRead" << std::endl;
outs.push_back("dummy_data2");
//return error if get some data
return outs.empty();
}
在我们的代码示例中,我们通过“克隆工厂”预先创建 reader 对象,该工厂将对象与其名称进行映射。客户端调用 Factory 的 create 函数并通过名称获取派生类。有关更高级的 Factory 示例,请参阅 [1,§8]。
最后,这是 TFP 情况下的原型图
最后的 remarks
TFP 惯用法可以与几种模式结合使用:原型、命令、模板方法等 [9],当您的派生类在行为上有所不同时。
该技术的主要限制是,您无法在不进行完全模板特化的情况下引入派生类的其他成员。
Alexandrescu [1] 使用“虚假”或空模板参数来指定带有模板模板参数的策略。空标签的理念来自 STL 库,其中标签约定用于设计迭代器 [7]。
据我所知,TFP 惯用法技术尚未发表。
我希望这项技术能在您的项目中有所帮助。
致谢
我要感谢 Philip Eskelin 讨论了本文的主题。感谢我的儿子 Tim Kunisky 帮助准备了本文。
历史
2007年8月23日
删除了声明(感谢我的同事 Alex Urben 发现了错误。)
:
使用宏定义,任何派生类都是最终类。例如,我们无法从 XSerializationException 类的宏实现派生。
参考文献
- [1] Andrei Alexandrescu, Modern C++ Design: Generic Programming and Design Patterns Applied. Addison-Wesley, 2001
- [2] David Vandevoorde, Nicolai M. Josuttis, C++ Templates: The Complete Guide. Addison-Wesley, 2002
- [3] Scott Meyers, Effective C++ (3rd edition). Addison-Wesley, 2005
- [4] Stephen C. Dewhurst, C++ Common knowledge. Addison-Wesley, 2005
- [5] International Standard for C++, ISO/IEC, 1998
- [6] Xerces-C++ parser, http://xml.apache.org/xerces-c/
- [7] Bjarne Stroudstrup, The C++ Programming Language (3r d edition), Addison-Wesley, 1998
- [8] Herb Sutter, Andrei Alexandrescu, C++ Coding Standards, Addison-Wesley, 2005
- [9] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns, Addison-Wesley, 1995.
- [10] Herb Sutter, Exceptional C++, Addison-Wesley, 2002