Operatorerna == och =

En praktisk operator som ännu inte överlagrats är jämförelseoperatorn ==. Vill vi med vår nuvarande definition av klassen Vector jämföra två vektorer måste vi manuellt jämföra varje medlem, vilket är fult och arbetsdrygt. Istället överlagrar vi operatorn == och implementerar den nödvändiga funktionaliteten:

// prototyp
bool operator== (const Vector & V) const;

// implementation
bool Vector::operator== (const Vector & V) const {
  // jämför V och denna vektor
  if ( m_X == V.m_X && m_Y == V.m_Y && m_Z == V.m_Z ) {
    // vektorerna är lika
    return true;
  }

  // de är olika
  return false;
}

Ett program som använder sig av == kan se ut på följande sätt:

#include <iostream>
#include "Vector8.h"

int main () {
  Vector V1 ( 1, 2, 3 );
  Vector V2 ( 6, 5, 4 );
  Vector V3 ( 1, 2, 3 );
 
  // jämför V1 och V2
  if ( V1 == V2 )
    cout << "V1 == V2" << endl;
  else
    cout << "V1 != V2" << endl;

  // jämför V1 och V3
  if ( V1 == V3 )
    cout << "V1 == V3" << endl;
  else
    cout << "V1 != V3" << endl;
}

Kör man programmet får man följande utskrift:

% ./TestVector8
V1 != V2
V1 == V3
%

Enda stora skillnaden mellan implementationen av denna överlagrade operator och de tidigare överlagrade är returtypen som är bool. Men vid närmare eftertanke är det ju den enda möjliga returntypen för en jämförelse som ju kan antingen vara sann eller falsk. Vi kan på samma gång överlagra även != för att göra det lätt att jämföra vektorer med varandra:

// prototyp
bool operator!= (const Vector & V) const;

// implementation
bool Vector::operator!= (const Vector & V) const {
  // använd negerad jämförelse
  return ! ( *this == V );
}

Koden ovan kan behöva några förklarande ord. Vi använder oss av den tidigare överlagrade operatorn == så att vi inte behöver skriva en ny och lång if-sats, men negerar det resultat som vi får. Så om de två vektorerna är lika varandra så är det inte olika varandra. Vi utför alltså först en jämförelse och sedan negeras resultatet. Här får vi nytta av att kunna referera till nuvarande objekt via pekaren this. Eftersom det inte finns någon överlagrad operator för att hantera pekare till vektorer måste vi först avreferera this, och först sedan utföra jämförelsen med V. Parentesen runt jämförelsen är nödvändig, eftersom ! har högre precedens än ==, och följdaktigen försöker kompilatorn först negera *this och sedan utföra jämförelsen. Vi vill dock göra det hela i motsatt ordning, därav parentesen.

Tilldelningsoperatorn =

En operator som vi egentligen inte behöver överlagra för vår enkla klass är operatorn =, d.v.s. normal tilldelning. Kompilatorn skapar alltid automatiskt en version av tilldelning ifall programmeraren inte gör det, och för klassen Vector skulle den versionen duga mycket väl. Det som den kompilatorgenererade versionen gör är att den skapar kod för att kopiera alla medlemmar från originalobjektet till det kopierade objektet. En dylik approach fungerar fint för vår klass, men kanske inte för en mer avancerad klass som innehåller pekare och/eller allokerat minne. Se t.ex. avsnittet Mera om copy-konstruktor i Kapitel 14 för ett exempel på en klass som inte kan använda den normala tilldelningen utan att orsaka problem. Notera att klassen där även heter Vector, men där avses en container, inte en matematisk vektor.

Vi skall dock explicit överlagra operatorn = i demonstrationssyfte. För andra mer komplicerade klasser är definitionen i princip samma, men den egentliga koden inne i metoden skiljer sig förstås. Vi får följande metod:

// protoyp
Vector & operator= (const Vector & V);

// implementation
Vector & Vector::operator= (const Vector & V) {
  // tilldelas vi värdet av oss själva?
  if ( this == &V ) {
    // jep, gör inget i så fall
    return *this;
  }
  
  // utför tilldelning
  m_X = V.m_X;
  m_Y = V.m_Y;
  m_Z = V.m_Z;

  // returnera oss själva
  return *this;
}

Denna metod kräver även lite förklaring. För det första är returvärdet i denna metod en referens till en Vector. Alla andra metoder har returnerat en "vanlig" Vector och inte en referens. Skillnaden ligger i att de hade i så fall returnerar en referens till en lokal variabel, som ju skall förstöras direkt då metoden är avslutad. En referens till ett förstört objekt är inte giltig, därför returneras en kopia. Men vid överlagring av operatorn = returneras inte en referens till en loka variabel, utan till själva klassen för vilken metoden anropats, d.v.s. this. Vi avrefererar this för att returnera en Vector och inte en Vector *. Tilldelningen sker till aktuellt objekt, precis som med operatorn +=.

De första raderna i metoden kontrollerar att vi inte försöker tilldela objektet till sig själv, d.v.s. göra något i stil med:

Vector V1 ( 1, 2, 3 );

// tilldela till sig själv
V1 = V1;

Om så görs skall inget speciellt göras. Man kan kontrollera om adressen för V är samma som this. Om så är fallet är det samma objekt, eftersom de finns på samma adress i minnet.

Den skarpsynte funderar nu säkert varför måste vi returnera en referens till en Vector då vi överlagrar tilldelning, räcker det inte med att enbart kopiera data från ursprungsvektorn? Orsaken till detta är att man i C++ (och C) kan skriva multipla tilldelningar på samma rad, t.ex. är följande giltigt:

Vector V1, V2, V3;

// multipel tilldelning
V1 = V2 = V3 = Vector (1, 1, 1);

Här vill vi att V1, V2 och V3 skall alla få värdet av den sista vektorn. Tilldelningarna utförs från höger till vänster, så V1 tilldelas sist. Normalt brukar man försöka undvika att använda multipel tilldelning, men eftersomd et är laglig C++ måste även en överlagrad operator = understöda syntaxen. Om vi således inte returnerar en Vector i vår metod så "stoppas" sekvensen av tilldelningar.

Vi har nu definierat de flesta operatorer som är vettiga för en vektor. Man kan även lägga till annan funktionalitet såsom t.ex. rotationer, kryssprodukter o.s.v., men de lämnas som en övning till läsaren :-)