zoukankan      html  css  js  c++  java
  • C++ 重载操作符与转换

    《C++ Primer 4th》读书笔记

    重载操作符是具有特殊名称的函数:保留字 operator 后接需定义的操作符号。

    Sales_item operator+(const Sales_item&, const Sales_item&);

    声明了加号操作符,可用于将两个 Sales_item 对象“相加”并获得一个 Sales_item 对象的副本。

     

    不能重载的操作符

    ::

     .*

     .

    ?:

    通过连接其他合法符号可以创建新的操作符。例如,定义一个 operator**以提供求幂运算是合法的。

     

    重载操作符必须具有至少一个类类型或枚举类型的操作数。这条规则强制重载操作符不能重新定义用于内置类

    型对象的操作符的含义。

     

    操作符的优先级(第 5.10.1 节)、结合性或操作数目不能改变。不管操作数的类型和操作符的功能定义如何,表达式

    x == y +z;

    总是将实参 y 和 z 绑定到 operator+,并且将结果用作 operator== 右操作数。

     

    重载操作符并不保证操作数的求值顺序,尤其是,不会保证内置逻辑 AND、逻辑 OR(第 5.2 节)和逗号操作符的操作数求值。在 && 和 ||的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定。

     

    大多数重载操作符可以定义为普通非成员函数或类的成员函数。作为类成员的重载函数,其形参看起来比操作数数目少 1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数。

     

    一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员。

    // member binary operator: left-hand operand bound to implicit this pointer
    
    Sales_item& Sales_item::operator+=(const Sales_item&);
    
    // nonmember binary operator: must declare a parameter for each operand
    
    Sales_item operator+(const Sales_item&, const Sales_item&);

    操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元。在这种情况下,操作符通常需要访问类的私有部分。

     

    使用重载操作符的方式,与内置类型操作数上使用操作符的方式一样。也可以像调用普通函数一样调用重载操作符函数,指定函数并传递适当类型适当数目的形参。

    cout << item1 + item2 << endl;
    
     
    
    // equivalent direct call to nonmember operator function
    
    cout << operator+(item1, item2) << endl;

    调用成员操作符函数与调用任意其他函数是一样的:指定运行函数的对象,然后使用点或箭头操作符获取希望调用的函数,同时传递所需数目和类型的实参。对于二元成员操作符函数的情况,我们必须传递一个操作数:

    item1 += item2;

     

    重载操作符的设计

    不要重载具有内置含义的操作符

    • 合成赋值操作符进行逐个成员赋值:使用成员自己的赋值:使用成员自己的赋值操作依次对每个成员进行赋值。

    • 默认情况下,取地址操作符(&)和逗号操作符(,)在类类型对象上的执行,与在内置类型对象上的执行一样。取地址操作符返回对象的内存地址,逗号操作符从左至右计算每个表达式的值,并返回最右边操作数的值。

    • 内置逻辑与(&&)和逻辑或(||)操作符使用短路求值。如果重新定义该操作符,将失去操作符的短路求值特征。

    重载逗号、取地址、逻辑与、逻辑或等等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们

    定义了自己的版本,就不能再使用这些内置含义。

     

    大多数操作符对类对象没有意义

    为类设计操作符,最好的方式是首先设计类的公用接口。定义了接口之后,就可以考虑应将哪些操作符定义为重载操作符。那些逻辑上可以映射到某个操作符的操作可以考虑作为候选的重载操作符。例如:

    • 相等测试操作应使用 operator==。

    • 一般通过重载移位操作符进行输入和输出。

    • 测试对象是否为空的操作可用逻辑非操作符 operator! 表示。

     

    复合赋值操作符

    如果一个类有算术操作符或位操作符,那么,提供相应的复合赋值操作符一般是个好的做法。

     

    相等和关系操作符

    将要用作关联容器键类型的类应定义 < 操作符。关联容器默认使用键类型的 < 操作符。即使该类型将只存储在顺序容器中,类通常也应该定义相等(==)和小于(<)操作符。

    如果类定义了相等操作符,它也应该定义不等操作符 !=。如果类定义了 <,则它可能应该定义全部的四个关系操作符(>,>=,<,<=)。

     

    选择成员或非成员实现

    • 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。

    • 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。

    • 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。

    • 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。

     

    警告:审慎使用操作符重载

    当一个重载操作符的含义不明显时,给操作取一个名字更好。对于很少用的操作,使用命名函数通常也比用操作符更好。如果不是普通操作,没有必要为简洁而使用操作符。

     

    输入和输出操作符

    支持 I/O 操作的类所提供的 I/O 操作接口,一般应该与标准库 iostream为内置类型定义的接口相同,因此,许多类都需要重载输入和输出操作符。

     

    输出操作符<< 的重载:为了与 IO 标准库一致,操作符应接受 ostream& 作为第一个形参,对类类型 const 对象的引用作为第二个形参,并返回对ostream 形参的引用。

    ostream& operator<<(ostream& out, const Sales_item& s)
    
    {
    
    out << s.isbn << "	" << s.units_sold << "	"
    
    << s.revenue << "	" << s.avg_price();
    
    return out;
    
    }

    一般而言,输出操作符应输出对象的内容,进行最小限度的格式化,它们不应该输出换行符。

    当定义符合标准库 iostream 规范的输入或输出操作符的时候,必须使它成为非成员操作符

     

    输入操作符>> 的重载:输入操作符的第一个形参是一个引用,指向它要读的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入的对象的非const 引用,该形参必须为非 const,因为输入操作符的目的是将数据读到这个对象中。

    更重要但通常重视不够的是,输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性。

     

    算术操作符和关系操作符

    为了与内置操作符保持一致,加法返回一个右值,而不是一个引用。

    Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs)
    
    {
    
    Sales_item ret(lhs); // copy lhs into a local object that we'll
    
    return
    
    ret += rhs; // add in the contents of rhs
    
    return ret; // return ret by value
    
    }

    相等操作符所包含的设计原则:

    • 如果类定义了 ==      操作符,该操作符的含义是两个对象包含同样的数据。
    • 如果类具有一个操作,能确定该类型的两个对象是否相等,通常将该函数定义为 operator==      而不是创造命名函数。用户将习惯于用 == 来比较对象,而且这样做比记住新名字更容易。
    • 如果类定义了      operator==,它也应该定义 operator!=。用户会期待如果可以用某个操作符,则另一个也存在。
    • 相等和不操作符一般应该相互联系起来定义,让一个操作符完成比较对象的实际工作,而另一个操作符只是调用前者。

    定义了 operator== 的类更容易与标准库一起使用。有些算法,如 find,默认使用 == 操作符,如果类定义了 ==,则这些算法可以无须任何特殊处理而用于该类类型。

     

    赋值操作符

    类赋值操作符必须是类的成员,以便编译器可以知道是否需要合成一个。可以为一个类定义许多附加的赋值操作符,这些赋值操作符会因右操作符类型不同而不同。

    // illustration of assignment operators for class string
    
    class string {
    
    public:
    
    string& operator=(const string &); // s1 = s2;
    
    string& operator=(const char *); // s1 = "str";
    
    string& operator=(char); // s1 = 'c';
    
    // ....
    
    };

    赋值必须返回对 *this 的引用string 赋值操作符返回 string 引用,这与内置类型的赋值一致。一般而言,赋值操作符与复合赋值操作符应返回操作符的引用。

     

    下标操作符

    可以从容器中检索单个元素的容器类一般会定义下标操作符,即operator[]。标准库的类 string 和 vector 均是定义了下标操作符的类的例子。下标操作符必须定义为类成员函数。

    定义下标操作符比较复杂的地方在于,它在用作赋值的左右操作符数时都应该能表现正常。只要下标操作符返回引用,就可用作赋值的任意一方。

    类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用。

    class Foo {
    
    public:
    
    int &operator[] (const size_t);
    
    const int &operator[] (const size_t) const;
    
    // other interface members
    
    private:
    
    vector<int> data;
    
    // other member data and private utility functions
    
    };

    下标操作符本身可能看起来像这样:

    int& Foo::operator[] (const size_t index)
    
    {
    
    return data[index]; // no range checking on index
    
    }
    
    const int& Foo::operator[] (const size_t index) const
    
    {
    
    return data[index]; // no range checking on index
    
    }

    成员访问操作符

    为了支持指针型类,例如迭代器,C++ 语言允许重载解引用操作符(*)和箭头操作符(->))。

    箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的。

    // private class for use by ScreenPtr only
    
    class ScrPtr {
    
    friend class ScreenPtr;
    
    Screen *sp;
    
    size_t use;
    
    ScrPtr(Screen *p): sp(p), use(1) { }
    
    ~ScrPtr() { delete sp; }
    
    };
    
     
    
    /*
    
    * smart pointer: Users pass to a pointer to a dynamically allocated Screen, which
    
    * is automatically destroyed when the last ScreenPtr goes away
    
    */
    
    class ScreenPtr {
    
    public:
    
    // no default constructor: ScreenPtrs must be bound to an object
    
    ScreenPtr(Screen *p): ptr(new ScrPtr(p)) { }
    
    // copy members and increment the use count
    
    ScreenPtr(const ScreenPtr &orig):
    
    ptr(orig.ptr) { ++ptr->use; }
    
    ScreenPtr& operator=(const ScreenPtr&);
    
    // if use count goes to zero, delete the ScrPtr object
    
    ~ScreenPtr() { if (--ptr->use == 0) delete ptr; }
    
    private:
    
    ScrPtr *ptr; // points to use-counted ScrPtr class
    
    };
    
     
    
    ScreenPtr p1; // error: ScreenPtr has no default constructor
    
    ScreenPtr ps(new Screen(4,4)); // ok: ps points to a copy of myScreen
    
     
    
    class ScreenPtr {
    
    public:
    
    // constructor and copy control members as before
    
    Screen &operator*() { return *ptr->sp; }
    
    Screen *operator->() { return ptr->sp; }
    
    const Screen &operator*() const { return *ptr->sp; }
    
    const Screen *operator->() const { return ptr->sp; }
    
    private:
    
    ScrPtr *ptr; // points to use-counted ScrPtr class
    
    };

    箭头操作符与众不同。箭头操作符不接受显式形参。因为 -> 的右操作数不是表达式,相反,是对应着类成员的一个标识符。没有明显可行的途径将一个标识符作为形参传递给函数,相反,由编译器处理获取成员的工作。

    当这样编写时:

    point->action();

    由于优先级规则,它实际等价于编写:

    (point->action)();

    换句话说,我们想要调用的是对 point->action 求值的结果。编译器这样对该代码进行求值:

    1. 如果 point 是一个指针,指向具有名为 action 的成员的类对象,则编译器将代码编译为调用该对象的 action 成员。

    2. 否则,如果 action 是定义了 operator-> 操作符的类的一个对象,则point->action 与 point.operator->()->action 相同。即,执行 point的 operator->(),然后使用该结果重复这三步。

    3. 否则,代码出错。

    重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。

     

    自增操作符和自减操作符

    自增(++)和自减(--)操作符经常由诸如迭代器这样的类实现,这样的类提供类似于指针的行为来访问序列中的元素。C++ 语言不要求自增操作符或自减操作符一定作为类的成员,但是,因为这些操作符改变操作对象的状态,所以更倾向于将它们作为成员。

    /*
    
    * smart pointer: Checks access to elements throws an out_of_range
    
    * exception if attempt to access a nonexistent
    
    element
    
    * users allocate and free the array
    
    */
    
    class CheckedPtr {
    
    public:
    
    // no default constructor; CheckedPtrs must be bound to an object
    
    CheckedPtr(int *b, int *e): beg(b), end(e), curr(b) { }
    
    // dereference and increment operations
    
    private:
    
    int* beg; // pointer to beginning of the array
    
    int* end; // one past the end of the array
    
    int* curr; // current position within the array
    
    };

    前缀式操作符的声明看起来像这样:

    class CheckedPtr {
    
    public:
    
    CheckedPtr& operator++(); // prefix operators
    
    CheckedPtr& operator--();
    
    // other members as before
    
    };
    
     
    
    // prefix: return reference to incremented/decremented object
    
    CheckedPtr& CheckedPtr::operator++()
    
    {
    
    if (curr == end)
    
    throw out_of_range
    
    ("increment past the end of CheckedPtr");
    
    ++curr; // advance current state
    
    return *this;
    
    }
    
     
    
    CheckedPtr& CheckedPtr::operator--()
    
    {
    
    if (curr == beg)
    
    throw out_of_range
    
    ("decrement past the beginning of CheckedPtr");
    
    --curr; // move current state back one element
    
    return *this;
    
    }

    同时定义前缀式操作符和后缀式操作符存在一个问题:它们的形参数目和类型相同,普通重载不能区别所定义的前缀式操作符还是后缀式操作符。为了解决这一问题,后缀式操作符函数接受一个额外的(即,无用的)int 型

    形参。使用后缀式操作符进,编译器提供 0 作为这个形参的实参。

    class CheckedPtr {
    
    public:
    
    // increment and decrement
    
    CheckedPtr operator++(int); // postfix operators
    
    CheckedPtr operator--(int);
    
    // other members as before
    
    };
    
    // postfix: increment/decrement object but return unchanged value
    
    CheckedPtr CheckedPtr::operator++(int)
    
    {
    
    // no check needed here, the call to prefix increment will do the check
    
    CheckedPtr ret(*this); // save current value
    
    ++*this; // advance one element, checking the increment
    
    return ret; // return saved state
    
    }
    
    CheckedPtr CheckedPtr::operator--(int)
    
    {
    
    // no check needed here, the call to prefix decrement will do the check
    
    CheckedPtr ret(*this); // save current value
    
    --*this; // move backward one element and check
    
    return ret; // return saved state
    
    }

    为了与内置操作符一致,后缀式操作符应返回旧值(即,尚未自增或自减的值),并且,应作为值返回,而不是

    返回引用。

    CheckedPtr parr(ia, ia + size); // iapoints to an array of ints
    
    parr.operator++(0); // call postfix operator++
    
    parr.operator++(); // call prefix operator++

    一般而言,最好前缀式和后缀式都定义。只定义前缀式或只定义后缀式的类,将会让习惯于使用两种形式的用

    户感到奇怪。

     

    调用操作符和函数对象

    可以为类类型的对象重载函数调用操作符。一般为表示操作的类重载调用操作符。定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象。

    函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,由形参的数目或类型加以区别。

    struct absInt {
    
    int operator() (int val) {
    
    return val < 0 ? -val : val;
    
    }
    
     
    
    int i = -42;
    
    absInt absObj; // object that defines function call operator
    
    unsigned int ui = absObj(i); // calls absInt::operator(int)

    尽管 absObj 是一个对象而不是函数,我们仍然可以“调用”该对象,效果是运行由 absObj 对象定义的重载调用操作符,该操作符接受一个 int 并值并返回它的绝对值。

     

    函数对象经常用作通用算法的实参。

    计算有多少个单词长度在 6 字符以上。该解决方案的一个部分包括定义一个函数以确定给定 string 的长度是否大于 6 字符:

    // determine whether a length of a given word is 6 or more
    
    bool GT6(const string &s)
    
    {
    
    return s.size() >= 6;
    
    }
    
    vector<string>::size_type wc =
    
    count_if(words.begin(), words.end(), GT6);

    以上实现有个严重问题:它将 6 这个数字固化在 GT6 函数的定义中。count_if 算法运行只用一个形参且返回 bool 的函数。理想情况下,应传递string 和我们想要的长度进行测试。通过该方式,可以使用同一代码对不同长度的字符串进行计数。

    // determine whether a length of a given word is longer than a stored bound
    
    class GT_cls {
    
    public:
    
    GT_cls(size_t val = 0): bound(val) { }
    
    bool operator()(const string &s)
    
    { return s.size() >= bound; }
    
    private:
    
    std::string::size_type bound;
    
    };
    
    cout << count_if(words.begin(), words.end(), GT_cls(6))
    
    << " words 6 characters or longer" << endl;

    标准库定义了一组算术、关系与逻辑函数对象类。有两个一元函数对象类:一元减(negate<Type>))和逻辑非(logical_not<Type>))。其余的标准库函数对象都是表示二元操作符的二元函数对象类。这些标准库函数对象类型是在 functional 头文件中定义的。

    算术函数对象类型

     

    plus<Type>

    +

    minus<Type>

     -

    multiplies<Type>

    *

    divides<Type>

     /

    modulus<Type>

    %

    negate<Type>

    -

     

    关系函数对象类型

     

    equal_to<Type>

    ==

    not_equal_to<Type>

    !=

    greater<Type>

    >

    greater_equal<Type>

    >=

    less<Type>

    <

    less_equal<Type>

    <=

     

    逻辑函数对象类型

    logical_and<Type>

    &&

    logical_or<Type>

    |

    logical_not<Type>

    !

     

    每个函数对象类都是一个类模板,函数对象类的模板类型指定调用操作符的形参类型。

    // passes temporary function object that applies > operator to two strings
    
    sort(svec.begin(), svec.end(), greater<string>());

    标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器分为如下两类:

    1. 绑定器,是一种函数适配器,它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。

    2. 求反器,是一种函数适配器,它将谓词函数对象的真值求反。

    标准库定义了两个绑定器适配器:bind1st 和 bind2nd。每个绑定器接受一个函数对象和一个值。

    为了计算一个容器中所有小于或等于 10 的元素的个数,可以这样给 count_if传递值:

    count_if(vec.begin(), vec.end(),bind2nd(less_equal<int>(), 10));

    标准库还定义了两个求反器:not1 和 not2。你可能已经想到的,not1 将一元函数对象的真值求反,not2 将二元函数对象的真值求反。为了对 less_equal 函数对象的绑定求反,可以编写这样的代码:

    count_if(vec.begin(), vec.end(),not1(bind2nd(less_equal<int>(), 10)));

    转换与类类型

    可用一个实参调用的非 explicit 构造函数定义一个隐式转换。这种构造函数定义了到类类型的转换。除了定义到类类型的转换之外,我们还可以定义从类类型的转换。

     

    转换操作符是一种特殊的类成员函数。它定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内声明,在保留字 operator 之后跟着转换的目标类型:

    class SmallInt {
    
    public:
    
    SmallInt(int i = 0): val(i)
    
    {
    
    if (i < 0 || i > 255)
    
    throw std::out_of_range("Bad SmallInt initializer");
    
    }
    
    operator int() const { return val; }
    
    private:
    
    std::size_t val;
    
    };

    转换函数采用如下通用形式:

    operator type();

    type 表示内置类型名、类类型名或由类型别名定义的名字。对任何可作为函数返回类型的类型(除了 void 之外)都可以定义转换函数。一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类型是可以的。

     

    转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。虽然转换函数不能指定返回类型,但是每个转换函数必须显式返回一个指定类型的值。转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为 const 成员。

     

    使用类类型转换

    只要存在转换,编译器将在可以使用内置转换的地方自动调用它

    • 在表达式中:

    SmallInt si;
    
    double dval;
    
    si >= dval // si converted to int and then convert to double

    • 在条件中:

    if (si) // si converted to int and then convert to bool

    • 将实参传给函数或从函数返回值:

    int calc(int);
    
    SmallInt si;
    
    int i = calc(si); // convert si to int and call calc

    • 作为重载操作符的操作数:

    // convert si to int then call opeator<< on the int value
    
    cout << si << endl;

    • 在显式类型转换中:

    int ival;
    
    SmallInt si = 3.541; //instruct compiler to cast si to int
    
    ival = static_cast<int>(si) + 3;

    使用转换函数时,被转换的类型不必与所需要的类型完全匹配。必要时可在类类型转换之后跟上标准转换以获得想要的类型。

    SmallInt si;
    
    double dval;
    
    si >= dval // si converted to int and then convert to double

    类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换,则代码将出错。

    // class to hold unsigned integral values
    
    class Integral {
    
    public:
    
    Integral(int i = 0): val(i) { }
    
    operator SmallInt() const { return val % 256; }
    
    private:
    
    std::size_t val;
    
    };
    
    int calc(int);
    
    Integral intVal;
    
    SmallInt si(intVal); // ok: convert intVal to SmallInt and copy to si
    
    int i = calc(si); // ok: convert si to int and call calc
    
    int j = calc(intVal); // error: no conversion to int from Integral

    使用构造函数执行隐式转换的时候,构造函数的形参类型不必与所提供的类型完全匹配。

    void calc(SmallInt);
    
    short sobj;
    
    // sobj promoted from short to int
    
    // that int converted to SmallInt through the SmallInt(int) constructor
    
    calc(sobj);

    实参匹配和转换

    // unwise class definition:
    
    // multiple constructors and conversion operators to and from the built-in types
    
    // can lead to ambiguity problems
    
    class SmallInt {
    
    public:
    
    // conversions to SmallInt from int and double
    
    SmallInt(int = 0);
    
    SmallInt(double);
    
    // Conversions to int or double from SmallInt
    
    // Usually it is unwise to define conversions to multiple arithmetic types
    
    operator int() const { return val; }
    
    operator double() const { return val; }
    
    // ...
    
    private:
    
    std::size_t val;
    
    };
    
     
    
    void compute(int);
    
    void fp_compute(double);
    
    void extended_compute(long double);
    
    SmallInt si;
    
    compute(si); // SmallInt::operator int() const
    
    fp_compute(si); // SmallInt::operator double() const
    
    extended_compute(si); // error: ambiguous

    如果两个转换操作符都可用在一个调用中,而且在转换函数之后存在标准转换,则根据该标准转换的类别选择最佳匹配。

     

    void manip(const SmallInt &);
    
    double d; int i; long l;
    
    manip(d); // ok: use SmallInt(double) to convert the argument
    
    manip(i); // ok: use SmallInt(int) to convert the argument
    
    manip(l); // error: ambiguous

    当两个构造函数定义的转换都可以使用时,如果存在构造函数实参所需的标准转换,就用该标准转换的类型选择最佳匹配。

     

    当两个类定义了转换时的二义性

    class Integral;
    
    class SmallInt {
    
    public:
    
    SmallInt(Integral); // convert from Integral to SmallInt
    
    // ...
    
    };
    
    class Integral {
    
    public:
    
    operator SmallInt() const; // convert from SmallInt to Integral
    
    // ...
    
    };
    
    void compute(SmallInt);
    
    Integral int_val;
    
    compute(int_val); // error: ambiguous

    实参 int_val 可以用两种不同方式转换为 SmallInt 对象,编译器可以使用接受 Integral 对象的构造函数,也可以使用将 Integral 对象转换为SmallInt 对象的 Integral 转换操作。因为这两个函数没有高下之分,所以这

    个调用会出错。

    在这种情况下,不能用显式类型转换来解决二义性——显式类型转换本身既可以使用转换操作又可以使用构造函数,相反,需要显式调用转换操作符或构造函数:

    compute(int_val.operator SmallInt()); // ok: use conversion operator
    
    compute(SmallInt(int_val)); // ok: use SmallInt constructor

    避免二义性最好的方法是避免编写互相提供隐式转换的成对的类。

     

    警告:避免转换函数的过度使用

    与使用重载操作符一样,转换操作符的适当使用可以大大简化类设计者的工作并使得类的使用更简单。但是,有两个潜在的缺陷:定义太多转换操作符可能导致二义性代码,一些转换可能利大于弊。

    避免二义性最好的方法是,保证最多只有一种途径将一个类型转换为另一类型。做到这点,最好的办法是限制转换操作符的数目,尤其是,到一种内置类型应该只有一个转换。

    当转换操作符用于没有明显映射关系的类类型和转换类型之间时,容易引起误解,在这种情况下,提供转换函数可能会令类的使用者迷惑不解。

     

    函数重载确定由三步组成:

    1. 确定候选函数集合:这些是与被调用函数同名的函数。

    2. 选择可行的函数:这些是形参数目和类型与函数调用中的实参相匹配的候选函数。选择可行函数时,如果有转换操作,编译器还要确定需要哪个转换操作来匹配每个形参。

    3. 选择最佳匹配的函数。为了确定最佳匹配,对将实参转换为对应形参所需的类型转换进行分类。对于类类型的实参和形参,可能的转换的集合包括类类型转换

     

    转换操作符之后的标准转换:如果重载集中的两个函数可以用同一转换函数匹配,则使用在转换之后或之前的标准转换序列的等级来确定哪个函数具有最佳匹配。

     

    显式强制转换消除二义性:面对二义性转换,程序员可以使用强制转换来显式指定应用哪个转换操作:

    void compute(int);
    
    void compute(double);
    
    SmallInt si;
    
    compute(static_cast<int>(si)); // ok: convert and call compute(int)
    
     
    
    class SmallInt {
    
    public:
    
    SmallInt(int = 0);
    
    };
    
    class Integral {
    
    public:
    
    Integral(int = 0);
    
    };
    
    void manip(const Integral&);
    
    void manip(const SmallInt&);
    
    manip(10); // error: ambiguous

    显式构造函数调用消除二义性:调用者可以通过显式构造所需类型的值而消除二义性:

    manip(SmallInt(10)); // ok: call manip(SmallInt)
    
    manip(Integral(10)); // ok: call manip(Integral)

    在调用重载函数时,需要使用构造函数或强制类型转换来转换实参,这是设计拙劣的表现。

     

    重载、转换和操作符

    操作符的重载确定遵循常见的三步过程:

    1. 选择候选函数。

    2. 选择可行函数,包括识别每个实参的潜在转换序列。

    3. 选择最佳匹配的函数。

     

    警告:转换和操作符

    正确设计类的重载操作符、转换构造函数和转换函数需要多加小心。尤其是,如果类既定义了转换操作符又定义了重载操作符,容易产生二义性。下面几条经验规则会有所帮助:

    1. 不要定义相互转换的类,即如果类 Foo 具有接受类 Bar 的对象的构造函数,不要再为类 Bar 定义到类型 Foo 的转换操作符。

    2. 避免到内置算术类型的转换。具体而言,如果定义了到算术类型的转换,则

    o 不要定义接受算术类型的操作符的重载版本。如果用户需要使用这些操作符,转换操作符将转换你所定义的类型的对象,然后可以使用内置操作符。

    o 不要定义转换到一个以上算术类型的转换。让标准转换提供到其他算术类型的转换。最简单的规则是:对于那些“明显正确”的,应避免定义转换函数并限制非显式构造函数。

     

    class SmallInt {
    
    public:
    
    SmallInt(int = 0); // convert from int to SmallInt
    
    // conversion to int from SmallInt
    
    operator int() const { return val; }
    
    // arithmetic operators
    
    friend SmallInt
    
    operator+(const SmallInt&, const SmallInt&);
    
    private:
    
    std::size_t val;
    
    };

    现在,可以用这个类将两个 SmallInts 对象相加,但是,如果试图进行混合模式运算,将会遇到二义性问题:

    SmallInt s1, s2;
    
    SmallInt s3 = s1 + s2; // ok: uses overloaded operator+
    
    int i = s3 + 0; // error: ambiguous

    第一个加使用接受两个 SmallInt 值的 + 的重载版本。第二个加有二义性,问题在于,可以将 0 转换为SmallInt 并使用 + 的 SmallInt 版本,也可以将s3 转换为 int 值并使用 int 值上的内置加操作符。

    既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性。

     

     

     

     

  • 相关阅读:
    [NOIP2017]宝藏 子集DP
    [NOI2017]蔬菜 贪心
    hihoCoder#1698 : 假期计划 组合数
    KNIGHTS
    动态图连通性(线段树分治+按秩合并并查集)
    CF868F Yet Another Minimization Problem 分治决策单调性优化DP
    【POJ】【3308】Paratroopers
    【BZOJ】【3437】小P的牧场
    【BZOJ】【3156】防御准备
    【BZOJ】【1010】【HNOI2008】玩具装箱Toy
  • 原文地址:https://www.cnblogs.com/1zhk/p/5095902.html
Copyright © 2011-2022 走看看