之前看过一些批判C++的文章,大致意思是它包含了太多的“奇技淫巧”,并不是一门好的语言。我对这个“奇技淫巧”的描述颇感兴趣,因为按照批判者的说法,C++的一些特性恰巧可以让一些炫耀技术的同学有了炫耀的资本——毕竟路人皆知的东西却没什么好炫耀的。这又让我想起了《孔乙己》中关于“回”字有几种写法的描述。当时老师在上此课时,是抱着批判的态度去评价孔乙己的这种思想,而我却感觉到这其中必有一些有意思的文化在里面——或许是“回”字演变的历程能说明什么。所以“回”字相关的内容让我感觉孔乙己是个纯粹和可爱的人。(转载请指明出于breaksoftware的csdn博客)
写这个系列的博文,并不是我想对C++进行什么批判,也不是想对其进行辩护。只是想罗列一些有意思的东西,故取名拾趣。
首先我们看下一种比较常见的技术——类构造函数的隐式转换。这儿先说明下,之后的例子中,我会为了尽量突出主要内容,而忽略一些可以作为充分条件但非必要条件的东西,故设计的一些代码存在“不完善”的嫌疑。因为为了堵住所有漏洞,往往会让整个代码让人感觉其重心并非在我想介绍的技术上,而在“苦行僧”式的编程原则上。
我们知道C++是一个类型严格的语言,比如下面一个函数
void test_int_proxy(const int_proxy& v) {
printf("%d", v.value());
}
调用者对其传参也应该是一个int_proxy的对象,但是实际情况并非如此。那该如何表述,我个人觉得应该是:编译器对其传参应该是一个int_proxy对象。这两种表述的区别就是“调用者”和“编译器”的区别。我们来看一个实际例子,我们先假定int_proxy类这么定义:
class int_proxy {
public:
int_proxy(int n) : _m(n) {};
public:
int value() const {
return _m;
}
private:
int _m;
};
该类非常简单,它有一个带参数的构造函数,并使用参数列表形式初始化类的成员变量。
一般情况下我们都会这么调用test_int_proxy方法:
test_int_proxy(int_proxy(100));
这种写法我想没人会有异议,但是如果出现下面这种写法,就可能让人感觉不可接受了:
test_int_proxy(100);
然而,这种写法对上述类的定义来说是合法的!其效果和使用int_proxy控制住是一样的。这是为什么呢?这便是类构造函数的隐式转换技术。C++编译器认为test_int_proxy方法传入的应该是一个const类型的int_proxy对象,然而如果它发现参数不是该对象时,就会使用该类中可以使用该参数进行构造对象的方法构造出一个临时的对象。我们例子中传参100是个int型数据,而int_proxy正好有一个携带int参数的构造函数。稍微总结下类构造函数隐式转换的必要条件:
- 找不到传参类型严格对应的函数
- 找到传参类型严格匹配的类的构造函数
- 因为隐式转换构造出的是临时对象,所以不可修改,故触发隐式转换的函数的传参类型必须要使用const修饰
但是个人觉得这种“奇巧淫技”还是不用为好。比如我们代码中还有如下函数:
void test_int_proxy(const int& v) {
printf("%d", v + 100);
}
那么C++编译器会针对传100的调用上面这个过程。这样一个函数调用有两个匹配的调用方法就会产生不确定性——这儿指的不确定性并非是指编译器调用哪个方法的不确定性,而是指维护这段代码的人对上述代码做调整时容易忽略一些问题而导致的“人祸”。
再比如,我们在代码中加入下面类和方法
class int_proxy_2 {
public:
int_proxy_2(int n) : _m(n) {};
public:
int value() const {
return _m + 100;
}
private:
int _m;
};
void test_int_proxy(const int_proxy_2& v) {
printf("%d", v.value());
}
那么编译器不能确定隐式转换是要转换哪个类,更不知道是调用哪个test_int_proxy方法了。
限制类构造函数的隐式转换的方法也很简单,就是给对应的构造函数加上explict关键字
class int_proxy {
public:
explicit int_proxy(int n) : _m(n) {};
这样通过隐式转换而构造临时对象的图谋将会被察觉并禁止。