8/9

6. Operators

C++ allows to defined operators on objects (symbols such as '+', '-', '<<', etc). Operators are particularly useful to model operations on mathematical modeling objects (ex. vectors, etc).

Operator "+", "-", "*", "/"

These operators acts between two objects (binary operators). They can usually be implemented either as function, or as method of an object (choice of design) and doesn’t impact their use.

Consider an operator X acting between two objects a and b and called as a X b.
The associated function will be type operatorX(const type& a, const type& b), or implemented as a method type type::operatorX(const type& b) const.

(Note that const and references are not required for operator to work, and can be adapted to the need)

Example of implementation on vec3.

Implementing as functions

#include <iostream>

struct vec3
{
    float x;
    float y;
    float z;
};

// Addition between vec3
vec3 operator+(const vec3& a, const vec3& b)
{
    return {a.x+b.x, a.y+b.y, a.z+b.z};
}

// Substraction between vec3
vec3 operator-(const vec3& a, const vec3& b)
{
    return {a.x-b.x, a.y-b.y, a.z-b.z};
}

// Multiplication between vec3 and float
vec3 operator*(const vec3& a, float scalar)
{
    return {a.x*scalar, a.y*scalar, a.z*scalar};
}

// Division between vec3 and float
vec3 operator/(const vec3& a, float scalar)
{
    return {a.x/scalar, a.y/scalar, a.z/scalar};
}

void display(const vec3& v)
{
    std::cout<<v.x<<","<<v.y<<","<<v.z<<std::endl;
}

int main()
{
    vec3 a {1.0f, 2.5f, 3.5f};
    vec3 b {4.0f, -1.0f, 2.0f};

    display( a+b ); // 5, 1.5, 5.5
    display( a-b ); // -3, 3.5, 1.5
    display( a*3.5f ); // 3.5, 8.75, 12.25
    display( a/2.0f ); // 0.5, 1.25, 1.75

    return 0;
}

Implementing as methods

#include <iostream>

struct vec3
{
    float x;
    float y;
    float z;

    vec3 operator+(const vec3& b) const;
    vec3 operator-(const vec3& b) const;
    vec3 operator*(float scalar) const;
    vec3 operator/(float scalar) const;

};

// Addition between vec3
vec3 vec3::operator+(const vec3& b)
{
    return {x+b.x, y+b.y, z+b.z};
}

// Substraction between vec3
vec3 vec3::operator-(const vec3& b)
{
    return {x-b.x, y-b.y, z-b.z};
}

// Multiplication between vec3 and float
vec3 vec3::operator*(float scalar)
{
    return {x*scalar, y*scalar, z*scalar};
}

// Division between vec3 and float
vec3 vec3::operator/(float scalar)
{
    return {x/scalar, y/scalar, z/scalar};
}

void display(const vec3& v)
{
    std::cout<<v.x<<","<<v.y<<","<<v.z<<std::endl;
}

int main()
{
    vec3 a {1.0f, 2.5f, 3.5f};
    vec3 b {4.0f, -1.0f, 2.0f};

    display( a+b ); // 5, 1.5, 5.5
    display( a-b ); // -3, 3.5, 1.5
    display( a*3.5f ); // 3.5, 8.75, 12.25
    display( a/2.0f ); // 0.5, 1.25, 1.75

    return 0;
}

Note that in the case where the operator receives two different argument (ex. vec3 and float), there is no supposition of commutativity. Therefore vec3 * float and float * vec3 must be defined as two different functions.

6. Operators "+=", "-=", "*=", "/="

These operators are similar to the previous ones, but this time, they modify the first argument (ex. a+=b, a*=5.0f). Conventionally, the modified object is returned by these operators.

Example of implementation as a function

vec3& operator+=(vec3& a, const vec3& b)
{
    a.x += b.x;
    a.y += b.y;
    a.z += b.z;
    return a;
}

Example of implementation as a method

vec3& vec3::operator+=(const vec3& b)
{
    x += b.x;
    y += b.y;
    z += b.z;
    return *this;
}

Notes

  • The return type is a reference on the modified object.

  • The first argument of the function is a non const reference. The method is not const qualified as attributes are modified.

  • The method use the syntax *this. this is a pointer on the current object, thus *this is a reference on the current object.

All other operators -=, *=, /= can be implemented in the same way.

Operator "<<"

Let us suppose we want to be able to display a vec3 using the syntax std::cout << a. This can be done in defining the operator << between the type of std::cout, namely std::ostream, and a vec3.

#include <iostream>

struct vec3
{
    float x;
    float y;
    float z;
};

// Allows the use of std::cout << vec3{x,y,z}
std::ostream& operator<<(std::ostream& s, const vec3& v)
{
    s << v.x << " ," << v.y << " ," << v.z;
    return s;
}

int main()
{
    vec3 a {1.0f, 2.5f, 3.5f};
    std::cout << a << std::endl;

    std::cout << vec3 {5.0f, 1.2f, 5.1f} << std::endl;

    return 0;
}

Unary operator "-"

Suppose you want to negate an object a using the syntax -a. Contrary to previous operator, this operator acts on only one parameter (unary operator).

Example of implementation on vec3 object

  • As a function

vec3 operator-(const vec3& v)
{
    return {-v.x, -v.y, -v.z};
}
  • As a method

vec3 vec3::operator-() const
{
    return {-x, -y, -z};
}

Operator [] and ()

Operators [] and () can be defined as method of an object. They can, in particular, model the behavior of an custom indexed sequence even if the class doesn’t intrinsically provide such API.

Example of application on vec3

#include <iostream>

struct vec3
{
    float x;
    float y;
    float z;

    float operator[](int k) const;
};

// Allow the use of v[0], v[1], v[2] and abort the program on other index
float vec3::operator[](int k) const
{
    switch(k)
    {
        case 0: return x;
        case 1: return y;
        case 2: return z;
        default: abort();
    }
}

int main()
{
    vec3 a {1.0f, 2.5f, 3.5f};

    std::cout << a[1] << std::endl;

    return 0;
}

Note that the switch/case is a specific syntax able to replace a sequence of if/else.

The previous code allows to access to a copy of x, y, z using the syntax [0], [1], [2] (similar to "get" in Java). But cannot modify it. In the case you want to allow to write on these variables (similar to "set" in Java), you can return a reference to the value.

#include <iostream>

struct vec3
{
    float x;
    float y;
    float z;

    // get
    float operator[](int k) const;
    // set
    float& operator[](int k);
};


float vec3::operator[](int k) const
{
    switch(k)
    {
        case 0: return x;
        case 1: return y;
        case 2: return z;
        default: abort();
    }
}

// Different signature but similar implementation
float& vec3::operator[](int k)
{
    switch(k)
    {
        case 0: return x;
        case 1: return y;
        case 2: return z;
        default: abort();
    }
}


int main()
{
    vec3 a {1.0f, 2.5f, 3.5f};

    a[0] = a[1] + a[2];

    // Now a.x = 6
    std::cout << a.x << std::endl;

    return 0;
}

Note

  • Argument of operator[] is not necessarily an integer, and can be any class.

  • operator[] can only receive one argument, operator() can however receive any number of argument.

Exercise

  • Model a 2D vector and a 2x2 matrix object such that the following code compiles and given the expected result

#include <iostream>


struct vec2; // to be completed
struct mat2; // to be completed


int main()
{
    const vec2 v {1.0f, 2.0f};
    const mat2 M {1.0f, 1.5f,
                  2.5f, 3.0f};

    // Should display 1.0
    std::cout << M(0,0) << std::endl;
    // Should display 2.5
    std::cout << M(1,0) << std::endl;
    // Should display 1.5
    std::cout << M(0,1) << std::endl;


    const vec2 v2 = M*v;
    const mat2 M2 = M*M;


    std::cout << v2 << std::endl;
    std::cout << M2 << std::endl;


    return 0;
}