首页 > 学院 > 开发设计 > 正文

C++ 泛型编程系列讲座之实施

2019-11-17 05:40:15
字体:
来源:转载
供稿:网友

  你知道,当一个概念从一个专有名词变成一个普通名词时,说明它真正的深入人心了。比如Kleenex(面巾纸品牌,也指面巾纸),Xerox(施乐,复印机品牌,也指复印机)Q-Tips(化妆品品牌,也指化妆包),对吗?所以说,当我听说你可以在Visual C++.NET中使用“modern C++ design”时非常兴奋也就不希奇了。这里指的是——至少我这样认为——《Modern C++ Design》[1]所推行的基于模板的技术。

  在所有泛型编程文章中都有一个成功的要素,就是进行特有至通用的“文法升华。”比如,在ScopeGuard[2]中增加了放入“撤消”动作于正常执行路径而当一个复杂操作成功时解除“撤消”的常见技术之后,ScopeGuard就成为了“域守卫(scope guard)”。我大多数受欢迎的文章不完全由我完成,而是和Petru Marginean紧密合作的结果。而且让我更加兴奋的是再一次在本文中与他合作。

  本文中我们将讨论对应于发布状态的机制:实施(enforcement),这是方便实用的快速条件验证机制。非常类似于ScopeGuard,ENFORCE宏极大程度地减少了你需要使用在错误处理上的代码。ScopeGuard和ENFORCE可相互独立工作,但最好是一起使用。ENFORCE是意外的源头,ScopeGuard是意外的传播者。

  实施(Enforcements)

  设想你注视着面前的一大堆代码。你知道你要在这些代码中杀出一条血路,你也知道你必须写更多的新代码。

  一个好习惯是先浏览代码,试着找到一些通用模式。你要理解模式背后的概念。很有可能,所有这些都不是偶然发生的,而是同样的基本概念的不同表现形式,然后,你可以整理这些提取出的模式,这样你就可以扼要地表达所有的作为概念的实现体的模式

  现在先暂停,看一下你要分析的不同模式的大小。假如这个模式很大,比如作为基础部分涉及到了整个程序,那么你正在和结构模式(architectural patterns)打交道。

  假如模式是中等尺寸,横跨数个对象和/或函数,那么你碰到的是设计模式。

  假如模式非常小,只包括3-10行代码,那么你面前的的常用法(idiom)。

  最后,假如模式只有1-2行代码,你得到的东西叫做代码风格或格式。

  这四种根据规模划分的类型所涵盖的范围类似于建筑学。在建筑学中,基本结构的小瑕疵不要紧。在软件架构中,任何缺陷都可能毁掉整个“建筑”。相反,你需要在任何范围使用正确的技术来确保成功。假如你对小细节着迷而忽略大的蓝图,你会浪费才华于建立不能远观的复杂的阿拉伯式建筑。假如你只注重大的方面而忽视细节,你会得到粗糙的庞然大物。

  这是写软件异常困难的原因,这个困难是在其他领域工作的人们不能完全理解的。

  实施属于常用法范畴。具体点说,实施极大简化了错误检测代码而不会影响可读性和正常工作流的流畅性。

  想法来源于下列事实。你抛出一个意外非常可能是作为一个布尔值检测的结果,如下:

if (some test)
 throw SomeException(arguments);
  假如该意外在多处出现,为什么不把它放在一个小函数里呢:

template <class E, class A>
inline void Enforce(bool condition A arg)
{
 if (!condition) throw E(arg);
}
  可以这样使用它:

Widget* p = MakeWidget();
Enforce<std::runtime_error>(p != 0, “null pointer”);
Enforce<std::runtime_error>(cout != 0, “cout is in error”);
cout << p->ToString();
  目前为止一切顺利.现在,我们来分析几个重要方面。

  首先,被测条件不总是布尔型的,也可能是一个指针或整型。其次,非常可能你要在检测完之后马上使用被测值。比如,你可能希望在使用前确保一个指针非空,或你可能要在创建一个文件句柄后马上使用它。所以我们修改Enforce,这样它就有滤过机制来把接受的值传回:

template <class E, class A, class T>
inline T& Enforce(T& obj, A arg)
{
 if (!obj) throw E(arg);
  return obj;
}

template <class E, class A, class T>
inline const T& Enforce(const T& obj, A arg)
{
 if (!obj) throw E(arg);
  return obj;
}
  (两个版本是必须的,分别对应const和非const对象。)你可以增加两个重载版本来表示你通常会抛出的那种意外和它所带参数。

Template <class T>
Inline T& Enforce(T& obj, const char* arg)
{
 return this->Enforce<std::runtime_error, const char*, T>(obj,arg);
}

template <class T>
inline const T& Enforce(const T& obj, const char* arg)
{
 return this->Enforce<std::runtime_error, const char*, T>(obj,arg);
}
  假如你认为应该传入一个通用参数(信息)到std::runtime_error,调用可以进一步简化。你所需做的一些只是增加几个重载函数:


Template <class T>
Inline T& Enforce(T& obj)
{
 return this->EnforceMstd::runtime_error, const char*, T>(obj,“Enforcement error”);
}

template <class T>
inline const T& Enforce(const T& obj)
{
 return this->Enforce<std::runtime_error, const char*, T>(obj,“Enforcement error”);
}
  现在,随着这些简单的扩展,代码变得相当得有表现力:

Enforce(cout) << Enforce(MakeWidget())->ToString();
  在一行中你不单创建了一个widget对象并把它打印到控制台,你还标明在这个过程中可能会发生的任何错误!你也许还要自动释放创建的Widget对象,只需要在里面添加auto_ptr:

Enforce(cout) <<
Enforce(auto_ptr(MakeWidget()))->ToString();
  哇!非常好——非凡是拿它和其他解决方案相比较时。

  在不中断正常执行流程的情况下,Enforce漂亮地过滤掉错误。这样,Enforce提供了一个方便的手段来检查和清除错误情况。

  尽量让程序员方便地处理错误是非常重要的。这是因为错误处理经常,不幸的,被认为是白费力气的工作。经理们不会把错误处理作为考核标准。结果是,匆忙的,过度操劳的,预备不足的[3]程序员们在那咬着手指头期盼cout会总是处于一个良好的状态,MakeShape也决不返回空指针。但咬手指头并不是一个真正好的编程方法。 修饰Enforce

  上面代码中所显示的“Enforcement failed”信息并不是很有用,所以我们需要对它做点修改。幸运的是,Petru的灵感一发而不可收拾。“那个脑袋从不停止工作!”

  首先,包含在错误通知中的好的信息应该有不讨人喜欢的__FILE__和__LINE__。同时,能看到失败的表达式也会很有帮助。就象我们在Asserter[4]中所做的,我们建立一个小小的类来为我们保存这些信息:

template <class Ref>
class Enforcer
{
 Ref obj_;
 const char* const locus_;
 public:
  Enforcer(Ref obj, const char* locus) : obj_(obj), locus_(locus) {}
  Ref Enforce()
  {
   if (!obj_) throw std::runtime_error(locus_);
    return obj_;
  }
};
  obj_成员保存被检测的对象。Locus_成员是上述关于文件,行数,和表达式的信息。

  为什么我们把Enforcer的模板参数叫做Ref而不是传统的T?原因是我们要总是用一个引用类型(不是一个值类型)来实例化Enforce,这会在接下去减少我们很多重复劳动。(假如你曾经写过类似的对const和非const引用的函数,你就会知道我的意思)

  好吧,现在创建Enforcer对象,我们用一个小函数,这样我们可以轻松一点,让它来做类型推断:

template <typename T>
inline Enforcer<const T&>
MakeEnforcer(const T& obj, const char* locus)
{
 return Enforcer<const T&>(obj, locus);
}

template <typename T>
inline Enforcer<T&>
MakeEnforcer(const T& obj, const char* locus)
{
 return Enforcer<T&>(obj, locus);
}

  我们现在只需要给蛋糕裱上奶油——意料之中的宏。

  我们知道你讨厌宏,而且讨厌宏的不在少数,但我们更讨厌重复打__FILE__和__LINE__:

#define STRINGIZE(something) STRINGIZE_HELPER(something)
#define STRINGIZE_HELPER(something) #something
#define ENFORCE(eXP) /
MakeEnforcer((exp), "ExPRession '” #exp "' failed in '"/
__FILE__ "', line: "STRINGIZE(__LINE__)).Enforce()
  STRINGIZE和STRINGIZE_HELPER宏是预编译器必须的复杂过程来把__LINE__转换为数字。(不,#__LINE__没有用。)我从来都不完全知道这些宏怎样和为什么起作用(这和预编译器部分有关…啊,我脑海里开始涌现那悲惨的回忆!停下来,医生!)——而且,坦白说,我情愿去了解纽约城的下水道系统怎样运作也不想知道这里的细节。只要说STRINGIZE(__LINE__)产生了一个包含现在行数的字符串就足够了。那些这方面的专家[6]提供了完整的解释。

  本专栏的一贯传统是不涉及编译器特性,所以我们只顺便提一下STRINGIZE技巧在MSVC的预编译器上会产生类似于(__LINE__VAR+7)的神秘字符串。

  令人兴奋的一面是,Enforcer的初始化代价只有两个指针赋值那样低廉,同时却保存了非常有用的信息。你可以方便地增加关于文件日期和编译时间的信息,以及非标准的信息,比如__FUNCTION__。

  支持多个参数及ENFORCE的自定义判定条件

  ENFORCE是个很好的想法,但假如你用了某样东西却发现在实际应用中却不如文章中宣称的那么有用,你不会感到受欺骗了吗?

  我们会,并且我们已经发现ENFORCE中两个重要缺陷。

  首先,通常更需要在默认的文件名,行数,和表达式信息的基础上增加——或代之以——传入一个自定义字符串的功能。

  其次,ENFORCE只用!操作符来检测非零条件。然而,在真实应用中,有时候需要被检查的“错误”值不是零。许多使用整数返回值的API,包括在<io.h>中的标准C文件函数,返回-1来标识一个错误。另一些API使用一个符号常量。而COM使用更复杂的情况:假如返回值为零(就是S_OK),表示正常,假如返回值小于零,说明有一个错误,并且返回的实际值给出了错误的信息。假如返回值大于零,状态就是“带信息的成功”,就是说返回值中有一些有用的信息[5]。

  显然我们需要一个更灵活的检测和报告框架。我们需要能够在两个层面上配置实施(判定条件和参数传入机制),最好在编译时配置,这样实施机制比同等的手写代码不会有更多的开销。(有一个明智的检查总是需要做一下:当某种抽象应用于具体情况,是否能比得上非抽象的解决方法?)

  基于策略的(Policy-based)设计正适合于解决此问题。所以Enforce需要从一个简单的类改进为一个双策略参数的模板类。第一个策略是判定条件策略(处理检测事宜),第二个策略是抛出策略(处理构建和抛出意外对象)。


template<typename Ref, typename P, tyoename R>
class Enforcer
{
 …使用两个策略(看下一部分)…
}
  两个策略都有非常简单的接口。以下是默认策略:

strUCt DefaultPredicate
{
 template <class T>
 static bool Wrong(const T& obj)
 {
  return !obj;
 }
}
struct DefaultRaiser
{
 template <class T>
 static void Throw(const T&, const std::string& message, const char* locus)
 {
  throw std::runtime_error (message + ‘/n’ + locus);
 }
} 实现细节(和漂亮的技巧)

  好的,现在让Enforcer使用它的两个策略来检测值并抛出意外应该很简单。

  当出现错误时假如能让用户来指定任意信息格式将会非常有用;进一步,除非一个意外确实被抛出,这些信息(可能会影响运行速度)应该被避免。一些灵感加99%的汗水,我们设计了一个能够满足这些要求的机制。

  我们先展示代码然后做说明。最终的Enforcer类如下所示:

template <typename Ref, typename P, typename R>
class Enforcer
{
 public:
  Enforcer(Ref t, const char* locus) : t_(t), locus_(P::Wrong(t) ? locus : 0)
  {
  }
  Ref Operator*() const
  {
   if (locus_) R::Throw(t_, msg, locus_);
    return t_;
  }
  template <class MsgType>
  Enforcer& operator()(const MsgType& msg)
  {
   if (locus_)
   {
    //执行到这里我们就有的是时间,不必有太高效率
    std::ostringstream ss;
    ss <<msg;
    msg_ += ss.str ();
   }
   return *this;
  }
  private:
   Ref t_;
   std::string msg_;
   const char* const locus_;
 };
 template <class P, class R, typename T>
 inline Enforcer<const T&, P, R>
 MakeEnforcer(const T& t, const char* locus)
 {
  return Enforcer<const T&, P, R>(t, locus);
 }
 template <class P, class R, typename T>
 inline Enforcer<T&, P, R>
 MakeEnforcer(T& t, const char* locus)
 {
  return Enforcer<T&, P, R>(t, locus);
 }
 #define ENFORCER(exp) /
 *MakeEnforcer<DefaultPredicate, DefaultRaiser>(/(exp), “Expression ‘” #exp “’ failed in ‘ ” /
__FILE__ ‘”, line: “ STRINGIZE(__LINE__))

  非常好,这样Enforce定义了两个操作符函数:operator*和模板化的operator()。而且注重ENFORCE宏将“*”放在MakeEnforcer调用之前。所有这些的工作原理是什么?为什么要这些辅助代码?

  假设你写下如下代码:

Widget* pWidget = MakeWidget();
ENFORCE(pWidget);
  ENFORCE宏扩展为:

*MakeEnforcer<DefaultPredicate, DefaultRaiser>(fpWidget),“Expression ‘pWidget’ failed in ‘blah.cpp’, line: 7”)
  MakeEnforcer被调用后创建下面类型的一个对象:

Enforcer<const Widget*&, DefaultPredicate, DefaultRaiser>
  该对象的创建使用了两个参数的构造函数。请注重,locus_只有当P::Wrong(t)为真时才被初始化为一个非空指针。换句话说,locus_只有在意外应该被抛出时才指向有用信息,否则为空。

  对于被创建的对象调用operator*函数。不出意料的,假如locus_非空,R::Throw被调用,否则被检测的对象只是被传回。

  继续看一个更有趣的例子,代码如下:

Widget* pWidget = MakeWidget ();
ENFORCE(pWidget)(“This widget is null and it shouldn’t!”);
  此处当Enforcer对象被创建后,operator()被调用。该操作要么把传入信息添加到msg_成员变量中,要么假如pWidget非空即无错误出现就忽略所有。换句话说,正常执行路径与带一个检测的执行路径执行得一样快。这就是漂亮的地方——真正的工作只在发生错误的情况才做,

  因为operator()是模板化的并且使用一个std::ostringstream,它支持一切你可以传到cout去的东西。而且,operator()返回*this,这样你可以把对它的连续调用连接起来,下面例子展示了这一点:


int n = …;
Widget* pWidget = MakeWidget(n);
ENFORCE(pWidget)(“Widget number “)(n)(“ is null and it shouldn’t!”);
  我们不知道你感觉如何,反正我们对这个设计十分的满足。反过来说,谁会不喜欢一个简明的,富于表达力的,并且是高效的解决方案呢? 定制判定和抛出策略

  基于策略的Enforcer提供了重要的钩子来答应无限的变化。比如,检测句柄值不为-1(而不是检测不为零)只需要六行代码:

struct HandlePredicate
{
 static bool Wrong(long handle)
 {
  return handle == -1;
 }
};

#define HANDLE_ENFORCE(exp)/
*MakeEnforcer<HandlePredicate, DefaultRaiser>((exp), “Expression ‘” #exp “’ failed in ‘” / __FILE__”’, line : “ STRINGIZE(__LINE__))
  别忘了Enforce返回传入的值,这给了客户代码带来极大的表现力:

const int available = HANDLE_ENFORCE(_read(file, buffer, buflen));
  上面从一个文件读取数据,记录读取的字节数,并且标志可能出现的错误。一行搞定,酷吧!

  类似的,你能够为你的应用程序方便弟定义适合的新策略和XYZ_ENFORCE宏。对应每个错误编码规范可以有一个XYZ_ENFORCE宏。多数应用程序使用1到4种不同规范。实际操作中我们碰到过下列常用规范:

  1、我们上面讨论过的基本ENFORCE。用operator!检查并且抛出std::runtime_exception。

  2、HANDLE_ENFORCE。检测不为-1并且抛出某种意外。

  3、COM_ENFORCE。当结果为负就作为错误处理。抛出策略从COM返回代码中提取错误信息并且放入要被抛出的意外对象中。我们相信这个工具在写重要的COM应用程序时是真正的无价之宝

  4、CLIB_ENFORCE。许多在C标准库中的函数错误时返回零并要你来检查errno获知错误详情。假如有一个能够把errno转为文本并将文本放入要抛出的意外中的漂亮的Raiser策略,CLIB_ENFORCE就会非常完美。

  5、WAPI_ENFORCE。某些Windows API函数成功时返回零失败时返回负的错误代码。

  适应新的错误编码规范是一件非常简单的事情。

  结论

  我们发现值的自动实施检查极其有用,甚至于拥有它等于拥有乐趣,失去它寸步难行。实施不是把几行代码浓缩为一行代码那么简单。实施让你集中精力于应用的正常执行流,自然而灵活地过滤掉不需要的值。这种过滤是通过返回其传入参数的过滤函数实现的。

  用了几个宏技巧,付出了非常低的运行时开销,增加了有用信息

  通过模板参数的参数化实现了基于策略的设计,它不仅强大在理论上,而且实践中同样如此。基于策略的方法产生的设计有低运行时开销(和手写的if同样小)和能够适应大多数非凡错误编码规范的高可配置框架。

  我们已用Microsoft Visual C++.NET Everett Beta和gcc 3.2测试过附加代码。放心用吧!

  参考和注释

  [1] A. Alexandrescu. Modern C++ Design (Addison-Wesley Longman, 2001).
  [2] Andrei Alexandrescu and Petru Marginean. "Simplify your Exception-Safe Code"
  [3] 预备不足意思是让一名程序员接手先天不足的具体设计。
  [4] Andrei Alexandrescu. "Assertions"
  [5] 这个规范看上去很好。但不知什么原因演变成旧规范。在COM世界中,S_TRUE是零,S_FALSE是1。不要忽略这个信息!
  [6] http://www.jaggersoft.com/pubs/CVu10_1.Html

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表