7/9

7. Opérateurs

Il est possible en C++ de définir des opérateurs agissant sur des objets. Les opérateurs sont des symboles ('+', '-', '<<', etc) qui agissent en tant que fonctions ou méthodes.

7. Opérateurs '+', '-', '*', '/'

Ces opérateurs agissent entre deux objets: ce sont des opérateurs binaires. Ils peuvent généralement être implémentés, soit comme fonctions simples, soit comme méthodes d’une classe. Ce choix est un choix de design lors de leur implémentation, et reste transparent au niveau de leur utilisation.

Considérons un opérateur X agissant entre deux objets a, et b, sous la forme a X b.

  • La fonction simple associée à cet opérateur est type operatorX(type a, type b).

  • La méthode associée à cet opérateur est type type::operatorX(type b)

Rem. Dans le cas, où type est passé en tant que référence constante pour éviter le cout de la copie de l’objet type, on a alors type operatorX(const type& a, const type& b) pour la fonction simple, et type type::operatorX(const type& b) const pour la méthode.

Exemple d’implémentation sur un vec3

Opérateurs en tant que fonctions simples

#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;
}

Rem

  • Les opérateurs * et / agissent entre deux types différents (vec3 et float). Le code ne pré-suppose pas que les opérateurs sont commutatifs, ainsi la fonction vec3 operator*(const vec3& a, float scalar) correspond à un vec3 multiplié à un float. L’opération correspondant à la multiplication d’un float avec un vec3 serait définie quant à elle par vec3 operator*(float scalar, const vec3& a)

Opérateurs en tant que méthodes

#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;
}

7. Opérateurs '+=', '-=', '*=', '/='

Ces opérateurs permettent de modifier l’objet sur lequel l’opérateur est appliqué.

Exemple d’implémentation en tant que fonction simple

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

Exemple d’implémentation en tant que méthode

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

Rem.

  • Le premier élément de la fonction est nécessairement une référence non-const (et, dans le cas de la méthode, celle-ci est non-const puisque les attributs sont modifiés).

  • Ces opérateurs renvoient conventionnellement une référence vers l’objet modifié, permettant une syntaxe du type a += b += c (bien que rarement rencontrée). Pour cela, la méthode utilise la syntaxe *this (rappel: this est un pointeur sur l’objet courant).

7. Opérateurs '<<'

Supposons que l’on souhaite pouvoir afficher le contenu d’un vec3 en ligne de commande à l’aide de la syntaxe std::cout << a (où a est un vec3). Pour cela, on défini un opérateur << entre le type correspondant à std::cout, c’est à dire std::ostream, et un 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;
}

Opérateur unaire '-'

Supposons que l’on souhaite calculer l’opposé d’un objet a avec la syntaxe -a. Contrairement aux précédents opérateurs, celui-ci ne prend qu’un seul argument en entrée, il s’agit d’un opérateur unaire.

Exemple d’implémentation sur un objet vec3

  • En tant que fonction

vec3 operator-(const vec3& v)
{
    return {-v.x, -v.y, -v.z};
}
  • En tant que méthode

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

Opérateur [] et ()

Les opérateurs crochets et parenthèses peuvent modéliser un accès à des sous-éléments de l’objet. Ces opérateurs sont, en particulier, utiles pour permettre de modéliser un accès sous forme d’indexation tel que a[k] pour un vecteur, ou M(i,j) pour une matrice. Ces opérateurs doivent nécessairement être implémentés en tant que méthode de l’objet.

Exemple d’application sur un vec3 afin de modéliser une indexation de type a[0] (pour x), a[1] (pour y), et a[2] (pour z).

#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;
}

Rem l’utilisation du switch/case correspond ici à un cas spécifique qui pourrait être remplacé par une suite de if/else.

Renvoi de référence

Le code précédent permet d’accéder à la valeur (sous forme de copie) des coordonnées x, y, et z. Il s’agit d’un opérateur similaire à un get suivant l’habitude Java (ex. a.getX(), a.getY(), a.getZ()), mais plus proche d’une syntaxe mathématique/algorithmique.

Cette syntaxe ne permet cependant pas de modifier la variable. Il peut cependant être souhaitable de pouvoir écrire a[k]=valeur (similaire à un set tel que a.setX/Y/Z(valeur)). Il est possible de modéliser ce comportement en C++ en renvoyant, non plus une copie de la variable, mais une référence (non-const).

#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;
}

Notes

  • L’argument de l’opérateur [] n’est pas nécessairement un entier, mais peut être n’importe quel objet (string, float, etc).

  • L’opérateur [] ne peut recevoir qu’un unique argument. Par contre, l’opérateur () est plus général et peut recevoir un nombre arbitraire d’arguments, utile par exemple pour les objets multi-dimensionnels tels que les matrices ou tenseurs (il est aussi possible de ne passer aucun argument).

Exercice

Complétez les opérateurs manquants de mat2 (matrice \(2\times 2\)) permettant d’exécuter la fonction main, et d’obtenir les résultats attendus.

#include <iostream>

struct vec2
{
    float x;
    float y;
};

std::ostream& operator<<(std::ostream& stream, const vec2& v)
{
    stream << v.x <<" "<<v.y;
    return stream;
}

struct mat2
{
    float xx, xy;
    float yx, yy;

    // Implement missing member operator on mat2
    // ...
};

// Implement missing functions operators on mat2 and vec2
// ...



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;

    // Should display 4 8.5
    std::cout << v2 << std::endl;
    // Should display 4.75 6 10 12.75
    std::cout << M2 << std::endl;

    return 0;
}