Operator overloading in C ++

    Good day!

    The desire to write this article appeared after reading the post Overloading C ++ operators , because many important topics were not covered in it.

    The most important thing to remember is operator overloading, this is just a more convenient way to call functions, so you should not get involved in operator overloading. Use it only when it makes writing code easier. But, not so much that it made reading difficult. After all, as you know, code is read much more often than it is written. And do not forget that you will never be allowed to overload operators in tandem with built-in types, the possibility of overloading is only for custom types / classes.

    Overload Syntax


    The operator overload syntax is very similar to the definition of a function named operator @, where @ is the identifier of the operator (for example +, -, <<, >>). Consider the simplest example:
    class Integer
    {
    private:
        int value;
    public:
        Integer(int i): value(i) 
        {}
        const Integer operator+(const Integer& rv) const {
            return (value + rv.value);
        }
    };
    

    In this case, the operator is framed as a member of the class, the argument determines the value located on the right side of the operator. In general, there are two main ways to overload operators: global functions that are friendly to the class, or inline functions of the class itself. Which way, for which operator is better, we will consider at the end of the topic.

    In most cases, operators (except for conditional ones) return an object, or a reference to the type to which its arguments relate (if the types are different, then you decide how to interpret the result of calculating the operator).

    Overloading Unary Operators


    Let's look at examples of overloading unary operators for the Integer class defined above. At the same time, we define them as friendly functions and consider the decrement and increment operators:
    class Integer
    {
    private:
        int value;
    public:
        Integer(int i): value(i) 
        {}
        //унарный +
        friend const Integer& operator+(const Integer& i);
        //унарный -
        friend const Integer operator-(const Integer& i);
        //префиксный инкремент
        friend const Integer& operator++(Integer& i);
        //постфиксный инкремент
        friend const Integer operator++(Integer& i, int);
        //префиксный декремент
        friend const Integer& operator--(Integer& i);
        //постфиксный декремент
        friend const Integer operator--(Integer& i, int);
    };
    //унарный плюс ничего не делает.
    const Integer& operator+(const Integer& i) {
        return i.value;
    }
    const Integer operator-(const Integer& i) {
        return Integer(-i.value);
    }
    //префиксная версия возвращает значение после инкремента
    const Integer& operator++(Integer& i) {
        i.value++;
        return i;
    }
    //постфиксная версия возвращает значение до инкремента
    const Integer operator++(Integer& i, int) {
        Integer oldValue(i.value);
        i.value++;
        return oldValue;
    }
    //префиксная версия возвращает значение после декремента
    const Integer& operator--(Integer& i) {
        i.value--;
        return i;
    }
    //постфиксная версия возвращает значение до декремента
    const Integer operator--(Integer& i, int) {
        Integer oldValue(i.value);
        i.value--;
        return oldValue;
    }
    

    Now you know how the compiler distinguishes between prefix and postfix versions of decrement and increment. In the case when he sees the expression ++ i, the function operator ++ (a) is called. If he sees i ++, then operator ++ (a, int) is called. That is, the overloaded operator ++ function is called, and it is for this that the dummy int parameter is used in the postfix version.

    Binary operators


    Consider the syntax for overloading binary operators. Overload one operator that returns an l-value, one conditional operator and one operator that creates a new value (define them globally):
    class Integer
    {
    private:
        int value;
    public:
        Integer(int i): value(i) 
        {}
        friend const Integer operator+(const Integer& left, const Integer& right);
        friend Integer& operator+=(Integer& left, const Integer& right);
        friend bool operator==(const Integer& left, const Integer& right);
    };
    const Integer operator+(const Integer& left, const Integer& right) {
        return Integer(left.value + right.value);
    }
    Integer& operator+=(Integer& left, const Integer& right) {
        left.value += right.value;
        return left;
    }
    bool operator==(const Integer& left, const Integer& right) {
        return left.value == right.value;
    }
    

    In all of these examples, operators are overloaded for one type, however, this is not necessary. You can, for example, overload the addition of our type Integer and a Float defined in its likeness.

    Arguments and Return Values


    As you can see, the examples use different methods for passing arguments to functions and returning operator values.
    • If the argument is not changed by the operator, in the case of, for example, a unary plus, it must be passed as a reference to a constant. In general, this is true for almost all arithmetic operators (addition, subtraction, multiplication ...)
    • The type of return value depends on the nature of the statement. If the operator must return a new value, then it is necessary to create a new object (as in the case of a binary plus). If you want to prohibit changing the object as l-value, then you need to return it constant.
    • For assignment operators, you must return a link to the changed item. Also, if you want to use the assignment operator in constructions of the form (x = y) .f (), where the function f () is called for the variable x, after assigning it to y, then do not return a reference to the constant, just return the reference.
    • Boolean operators should return at worst int, and at best bool.


    Return Value Optimization


    When creating new objects and returning them from a function, use the record as for the above example of the binary plus operator.
    return Integer(left.value + right.value);

    Honestly, I don’t know which situation is relevant for C ++ 11, all the arguments below are valid for C ++ 98.
    At first glance, this is similar to the syntax for creating a temporary object, that is, as if there is no difference between the code above and this:
    Integer temp(left.value + right.value);
    return temp;
    

    But in fact, in this case, the constructor will be called on the first line, then the copy constructor will be called, which will copy the object, and then, when the stack is unwound, the destructor will be called. When using the first record, the compiler initially creates an object in memory in which to copy it, thus saving the call to the copy constructor and destructor.

    Special operators


    In C ++, there are operators that have specific syntax and overload method. For example, the index operator []. It is always defined as a member of the class, and since the behavior of the indexed object as an array is implied, it should return a reference.

    Comma operator

    The “special” operators also include the comma operator. It is called for objects with a comma next to it (but it is not called in function argument lists). To come up with a meaningful example of using this operator is not so simple. Habrauser AxisPod in the comments to the previous article on overload spoke about one thing .

    Pointer dereference operator

    Overloading these statements may be justified for smart pointer classes. This operator is necessarily defined as a function of the class, and some restrictions are imposed on it: it must return either an object (or a link) or a pointer that allows you to access the object.

    Assignment operator

    The assignment operator is necessarily defined as a function of the class, because it is inextricably linked to the object to the left of "=". Defining a global assignment operator would make it possible to override the standard behavior of the "=" operator. Example:
    class Integer
    {
    private:
        int value;
    public:
        Integer(int i): value(i) 
        {}
        Integer& operator=(const Integer& right) {
            //проверка на самоприсваивание
            if (this == &right) {
                return *this;
            }
            value = right.value;
            return *this;
        }
    };
    


    As you can see, at the beginning of the function, a check is made for self-assignment. In general, in this case, self-assignment is harmless, but the situation is not always so simple. For example, if the object is large, you can spend a lot of time on unnecessary copying, or when working with pointers.

    Non-Overloading Operators

    Some operators in C ++ are not overloaded in principle. Apparently, this is done for security reasons.

    • The operator to select a class member is ".".
    • The operator dereferencing a pointer to a member of the class ". *"
    • In C ++, there is no exponentiation operator (as in Fortran) "**".
    • It is forbidden to define your own operators (problems with prioritization are possible).
    • You cannot change operator priorities


    Guidelines for operator definition form


    As we already found out, there are two ways of operators - in the form of a class function and in the form of a friendly global function.
    Rob Murray, in his book C ++ Strategies and Tactics, outlined the following guidelines for choosing an operator form:
    Operator
    Recommended Form
    All unary operators
    Class member
    = () [] -> -> *
    Required class member
    + = - = / = * = ^ = & = | =% = >> = << =
    Class member
    The rest of the binary operators
    Not a member of the class


    Why is that? Firstly, some operators were initially limited. In general, if there is no semantical difference in how to define an operator, then it is better to design it as a class function in order to emphasize the connection, plus in addition the function will be inline. In addition, sometimes there may be a need to represent the left-hand operand as an object of another class. Probably the most striking example is the redefinition of << and >> for input / output streams.

    Literature


    Bruce Eckel - Philosophy of C ++. Introduction to standard C ++ .

    Also popular now: