3 non-explicit-one-argument ctor 具有一个实参的构造函数

3 non-explicit-one-argument ctor 具有一个实参的构造函数

non-explicit-one-argument ctor

class Fraction
{
public:
    Fraction(int num, int den = 1)
    : m_numerator(num), m_denominator(den){ }

    Fraction operator+(const Fraction& f){
        return Fraction(/*...*/);
    }
private:
    int m_numerator;
    int m_denominator;
};

这里的代码和上一节的代码有些不一样, 这里的代码没有那个转换函数

我们这里的 Fraction 构造函数相当特别, 我们特别把这一种构造函数叫作 non-explicit-one-argument ctor 具有一个实参的构造函数. 这个构造函数有两个参数, 但后面那个参数有默认值, 所以你创建对象的时候只用给一个参数就可以了.

这样的设计其实是合理的, 在数学上比如整数 3 其实就是 3/1, 分母为 1 当作默认值.

这里是 2 parameter 但是是 1 argument. 1 argument 的意思是只要一个实参就够了. 当然你给它两个实参也可以.

那么 non-explicit 是什么意思呢 ? explicit 是一个关键字, 可以出现在构造函数的前面, 这里没有加 explicit, 所以叫 'non-explicit-one-argument ctor'.

它有什么特性, 我们来看下面的使用示例代码

Fraction f(3, 5);
Fraction d2 = f + 4;    // 调用 non-explicit ctor 将 4 转为 Fraction(4, 1)
//然后调用 operator+

我创建了一个五分之三的对象, 然后让它加 4.

刚刚提到了, 编译器遇到你的一个语句, 它会想办法去找能不能让你这条语句通过. 它现在看到 +, 于是它就去找, 找到了你的运算符重载. 但是它发现这个 + 的动作是分数和分数相加, 你这里右边是一个整数 4, 这个形式和我的设计不同, 于是编译器会去看看能不能把 4 转换为 Fraction. 然后编译器发现有这么一个构造函数, 于是它就用这个函数把 4 转为 4/1 了. 这个时候就是分数与分数相加, 就可以调用 operator+ 函数了.

non-explicit-one-argument ctor 的作用是可以把别的东西, 转换为这种东西. 而上一节是把这种东西转换为别的东西, 方向正好相反.

conversion function vs. non-explicit-one-argument ctor

class Fraction
{
public:
    Fraction(int num, int den=1)
    : m_numerator(num), m_denominator(den){ }

    operator double() const {
        return (double)(m_numerator / m_denominator);
    }

    Fraction operator+(const Fraction& f){
        return Fraction(/*...*/);
    }
private:
    int m_numerator;
    int m_denominator;
}

如果转换函数和具有一个实参的构造函数并存, 并且下面应用的代码写成这样的话

Fraction f(3, 5);
Fraction d2 = f + 4;    // [Error] ambiguous 歧义, 二义

这个时候编译器就不知道该怎么办了

编译器是要把 4 转为 fraction 吗 ? 构造函数可以把 4 转换为 fraction, 然后也有 fraction 的运算符重载, 看起来这条路走得通. 但是这里的 operator double() 函数是把 fraction 转为 double, 4 + 0.6 = 4.6, 然后 4.6 转为 d2, d2 是一个 fraction. 然后编译器发现两条路都可行, 于是编译器就不知道该走哪一条了, 所里这里编译器就报错了

这里两个函数并存会导致 ambiguous, 然而在上一节的代码里, 两个函数也是并存的但是可以编译通过. 注意两者的区别.

explicit-one-argument ctor

explicit, 明白的明确的, 就是告诉编译器不要自动地帮我做事情

class Fraction
{
public:
    explicit Fraction(int num, int den=1)
    : m_numerator(num), m_denominator(den) { }

    operator double() const {
        return (double)(m_numerator / m_denominator);
    }

    Fraction operator+(const Fraction& f){
        return Fraction(/*...*/);
    }
private:
    int m_numerator;
    int m_denominator;
};

这里 Fraction 既然设计是构造函数, 那它就只是一个构造函数. 所以现在我把这里加上 explicit, 这样一来的话, 就是告诉编译器说, 你不可以自动地把 3 变成 3/1 这样

我们来看使用代码

Fraction f(3, 5);
Fraction d2 = f + 4;    // [Error] conversion from 'double' to 'Fraction' requested

现在是 3/5 加 4, 这个 4 就不会变成 4/1 了

于是这个加法就失败了, 因为这个加法的运算符重载函数里面写着左右两边都必须是分数 Fraction, 现在右边 4 转换不过去, 就报错了

这个编译器的报错信息是说, 编译器想从 double 转换成 Fraction, 意思是 4 要转为分数 4/1, 于是失败了

这个关键字用的地方其实很少, 只有在构造函数的前面用到. 其实在模板的一个很小的地方也用得到这个关键字, 这个地方太细节了也就很少人注意到它

所以基本上这个关键字是用在构造函数前面

conversion function, 转换函数

如果你很好奇标准库有没有哪个地方用到了转换函数, 这里就有一个例子

template<class Alloc>
class vector<bool, Alloc>   // 一个容器, 模板的偏特化
{
public:
    typedef __bit_reference reference;
protected:
    // 对操作符中括号重载
    /*
    *   这个容器的意思是说它里面放的每个都是 boolean 值, 所以比如说要取出第一百个位置上面的真或假, 传出来的东西应该是一个 boolean 值.
    *   但这里的设计是, 传出来的是 reference 这个东西, 这个是另外一个类 __bit_reference. 这个设计手法叫作代理, 用 reference 这个东西去代理代表它本来应该传回来的东西.
    * 所以 reference 这个东西自然应该有一个转换函数转为 boolean.
    * 你拿 A 去代表 B, 那 A 应该就有一个转换函数转为 B
    * __bit_reference 的源代码在下面
    */
    reference operator[](size_type n){
        return *(begin() + difference_type(n));
    }

    //...

};
struct __bit_reference{
    unsigned int* p;
    unsigned int mask;

    //...

public:
    // 转换函数, 转为 boolean 值
    operator bool() const { return !(!(*p & mask)); }

    //...

}