C++之虚函数的访问性

  在上次的一篇文章中,提到了private virtual函数,说实话直到当前自己所写的所有的虚函数都是public的,毕竟成员数据总应当被设置为private已经深入人心了,但是对成员函数的访问性貌似强调的不够多。后面网上搜了一下,虚函数的访问性还是挺有讲究的,顿时Sutter的两篇历史博文让自己醍醐灌顶,可见经典永流传啊。
  总体而言,涉及到虚函数应该秉持Non-Virtual Interface Idiom,相似的说法是Template Method涉及模式。

一、接口类型是non-virtual public,实现类型是virtual private

  如果一个成员函数是virtual public,那么这个函数就需要完成两个任务:定义调用接口、提供实现细节,而很多请看下这两个目标是相互对立的制约关系,因为接口要尽可能保持稳定,而实现要尽可能的方便修改更新。
  模板方法就是将接口定义为稳定的non-virtual,然后将实现和定制化的工作代理给private virtual成员函数,这样继承类就可以直接继承public函数作为稳定接口,同时override基类的private virtual进行定制化的实现。这样去做的话其好处有:
  (1) 在基类的公有接口中可以做很多pre-conditions和post-conditions的工作、插入度量性代码、写入调试跟踪日志等,跟一般的说是在调用之前设定好相关场景,而在调用之后清理相关场景,而不需要在每个派生类override的时候重复这一任务。
  (2) 接口和实现分类后,两者就不用像原本public virtual要实现一一对应的关系,比如在一个公共接口中可以按照一定的顺序、一定的条件可选择性的调用多个private virtual实现函数,派生类选择性的override某些或者全部虚函数,处理起来就更加灵活了。
  (3) 这样实现后的类后续修改和维护更加的方便,可以快捷的在public non-virtual接口中添加检查、调试等任何操作,派生类也可以按需独立的override业务部分,接口的使用者不受任何影响。
  (4) 关键的是这种手法几乎没有副作用,即使公有接口类没有额外的工作而仅仅当做一个函数wrapper,也可以使用inline进行可能的调用开销的优化。
  再次强调一次,函数的virtual和访问属性两者是正交独立的,即使基类的private virtual函数派生类不能访问,也不影响派生类对其override以提供定制化的行为实现,所以看到virtual同时又是private不要为此感到奇怪。如果在派生类的代码中需要直接调用这个虚函数,那么这个虚函数可以为protected的,不过通常上面的Template Method都足够使用了,所以protected virtual也是很少见的情况。在Sutter统计标准库看来,non-public的虚函数占比达到95%以上,所以如果你的代码还有public virtual成员函数,看到这些就应该考虑改掉这些习惯了,否则是不是太不专业了。
  总的说,virtual成员函数应当被当做成员变量来看待——尽可能的让他们成为private。

二、析构函数要么virtual public,要么non-virtual protected

  这个的结论是:如果有多态析构对象的需求,那么析构函数就应该是public virtual的,否则就应该是protected non-virtual的。
  其实析构函数在很多教材中都被狠狠的过分被强调:如果发生了继承关系,那么基类的虚函数就应该是virtual的。这句话不完全正确,首先我们需要明确的是,如果在某个环境下可以析构某个对象,那么该对象的析构函数必须是可访问的,因此一个普通类要想直接构造器对象,那么该环境下其析构函数必须是公有的,否则编译器编译的时候直接报错,所以既然虚函数也像一般成员函数一样受到多态和访问控制限制,下面讨论起来就方便多了。
  如果允许通过基类的指针、引用进行对象的析构,那么就需要将基类的析构函数定以为public virtual的,因为根据虚函数的法则如果其指向的是派生类的对象,会调用派生类的析构函数,派生类的析构完成后会再调用基类的析构函数,对象就被完整析构了;如果不允许通过基类的指针、引用进行析构,那么基类的析构函数就可以为non-virtual protected类型,这样对象就只允许派生类的指针、引用以及派生类的实体对象进行直接析构,而派生类的析构必然会调用基类的析构函数,此时基类的析构函数就无须是virtual的了。
  这里说到的基类如果涉及到多态删除,那么必然是引用或者指针来操作的,那如果基类是一个非虚类(Concrete Class),而基类直接是对象析构而不涉及多态的话怎么论呢?各位C++大佬早就强调了:如果一个类要作为基类,那么就不要让他成为Concrete Class(不允许直接创建对象),反过来说就是不要派生Concrete Class,所以Meyers也生称:“Make non-leaf classes abstract”。
  如果还要更直白的表述,那么就是:如果基类B有一个派生类D,如果有可能用户使用B*指向实际的D类型的对象,而且可能会对这个指针执行delete类似的析构操作,则基类B需要一个virtual public析构函数。
  然后大师给出的理由是:析构函数作为第一个虚函数,如果添加了根本就用不到的多态功能,将带来所有运行时的开销,尤其当这个类很小(而且还没有其他的虚函数),那么自动强制一个虚析构函数就会显得显式增加了很多额外的开销。C++信奉的是高效率,“只为需要用到的东西付出代价”……

三、小例子

  为了表述上面的NVII手法,用最近做的一个小工具可以说明。话说在公司时常被公司财务各种烦,问的问题也不复杂,就是把一个统计表的最新数据SELECT给他们核对和监测,财务没有数据库的权限(也不期望他们会查询操作数据库),奇葩的管理后台也没人做这个东西,所以不堪其扰的我自己写了一个服务,将常用几个展示需求列出来,用之前的HTTP服务框架写了一个服务,财务通过点不同的连接,就可以将后台数据库最新数据展示给他们。
  为了让展示效果看起来更美观一点,就需要做一写额外的修饰作用。大家知道,HTML的css样式和table相关标签是固定的,但是每个数据表的属性是动态的,所以通过NVII的方式可以在基类的non-virtual public函数中做相同的前置、后置的工作,而在派生类实现virtual private接口定址不同的内容。
  基类部分的定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class StatHandler {
public:

explicit StatHandler(const HttpParser& http_parser):
http_parser_(http_parser) {
}

int fetch_stat(std::string& str) {

ss_ << "<html>" << std::endl;
ss_ << "<meta http-equiv='Content-Type' content='text/html; charset=utf8'>" << std::endl;
ss_ << "<head>" << std::endl;

ss_ << "<style> "
"table { "
" font-family:\"Trebuchet MS\", Arial, Helvetica, sans-serif; "
" border-collapse: collapse; "
" table-layout: fixed; } "
"th, td { "
" text-align: left; "
" padding: 8px; }"
"tr:nth-child(even){background-color: #F2F2F2;} "
"tr:nth-child(odd) {background-color: #EAF2D3;} "
"</style>"
<< std::endl;
ss_ << "</head>" << std::endl;

ss_ << "<body>" << std::endl;
ss_ << "<table align=center>" << std::endl;

print_head();
if(print_items() != 0)
return -1;

ss_ << "</table>" << std::endl;
ss_ << "</body>" << std::endl;
ss_ << "</html>" << std::endl;

str = ss_.str();
return 0;
}

virtual ~StatHandler() {
}

private:
virtual void print_head() = 0;
virtual int print_items() = 0;

protected:
std::stringstream ss_;
const HttpParser& http_parser_;
};

  某一个派生类的定址操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class SubmitStatHandler: public StatHandler {
public:
explicit SubmitStatHandler(const HttpParser& http_parser)
:StatHandler(http_parser), bankpay_(false){
}

private:
virtual void print_head();
virtual int print_items();

private:
bool bankpay_;
std::string date_;
};

void SubmitStatHandler::print_head() override {
std::string date;
if (!http_parser_.get_request_uri_param("date", date)) {
date = time_to_date(::time(NULL));
} else {
if (date == "yesterday")
date = time_to_date(::time(NULL) - (24 * 60 *60));
// else
// input date
}

date_ = date;

ss_ << "<h3 align=\"center\">" << "打款系统提交实时统计 @" << date_ << "</h2>" << std::endl;

ss_ << "<tr style=\"font-weight:bold; font-style:italic;\">" << std::endl;

ss_ << "<td>" << "F_partner_id" << "</td>" << std::endl;
ss_ << "<td>" << "F_date" << "</td>" << std::endl;
ss_ << "<td>" << "F_channel" << "</td>" << std::endl;
ss_ << "<td>" << "F_amount" << "</td>" << std::endl;
ss_ << "<td>" << "F_count" << "</td>" << std::endl;

ss_ << "</tr>" << std::endl;
}

int SubmitStatHandler::print_items() override {

int nResult = 0;

log_debug("date_ -> %s", date_.c_str());

do {

sql_conn_ptr conn;
request_scoped_sql_conn(conn);

safe_assert(conn);
if (!conn){
log_err("Get SQL connection failed!");
nResult = -1;
break;
}

shared_result_ptr result;
result.reset(conn->sqlconn_execute_query(
va_format(" SELECT F_partner_id, F_date, F_channel, F_amount, F_count FROM paybank.t_partner_submit_stats WHERE F_date='%s'; ", date_.c_str())));

if (!result){
log_err("Failed to query trans order info" );
nResult = -2;
break;
}

std::string F_partner_id;
std::string F_date;
std::string F_channel;
int64_t F_amount;
int64_t F_count;

while(result->next()) {
if (!cast_raw_value(result, 1, F_partner_id, F_date, F_channel, F_amount, F_count)){
log_err("Failed to cast trans order info ..." );
nResult = -3;
break;
}

ss_ << "<tr>" << std::endl;

ss_ << "<td>" << F_partner_id << "</td>" << std::endl;
ss_ << "<td>" << F_date << "</td>" << std::endl;
ss_ << "<td>" << F_channel << "</td>" << std::endl;
ss_ << "<td>" << F_amount << "</td>" << std::endl;
ss_ << "<td>" << F_count << "</td>" << std::endl;

ss_ << "</tr>" << std::endl;

}

} while (0);

return nResult;
}

  通过上面的额方式,展示这类重复公共的信息,可以在StatHandler中折腾(仅一次)就可以了,而数据变动的部分则在虚函数中override重写就可以了。

  最后一句:真爱生命,原理财务……

  所以,C++的确很复杂,即使摸熟了其语法和各种潜规则,要想真枪实弹的上战场,还需要各种Design Pattern和Idioms才能保驾护航,真的让人感到是“唯有套路得人心”啊~

本文完!

参考