Operatory

Przeciążanie operatorów

C++ umożliwia definiowanie operatorów dla typów zdefiniowanych przez użytkownika.

+

-

*

/

%

^

&

|

~

!

=

<

>

+=

-=

*=

/=

%=

^=

&=

=

<<

>>

>>=

<<=

==

!=

<=

>=

&&

||

++

--

->*

,

->

[]

()

new

new[]

delete

delete[]

Operatory, które nie mogą być przeciążane.

.

::

.*

?:

sizeof

alignof

noexcept

typeid

Operatory dla klas zdefiniowanych przez użytkownika mogą być przeciążane jako:

  • operatorowa funkcja składowa klasy

  • globalna funkcja operatorowa

Funkcja operatorowa – funkcja o nazwie operator@, gdzie @ jest znakiem operatora.

void f(const Complex& a, const Complex& b)
{
    Complex c = a + b;
    Complex d = a.operator+(b);
}

Operatory dwuargumentowe – binarne

Operator binarny aa@bb może być zdefiniowany jako:

  • niestatyczna metoda przyjmująca jeden argument aa.operator@(bb)

  • funkcja globalna przyjmująca dwa argumenty operator@(aa, bb)

class X
{
public:
    X(int);
    void operator+(int);
};

void operator+(X, X);
void operator+(X, double);
void f(X a)
{
    a + 1;    // a.operator(1)
    1 + a;    // ::operator+(X(1), a)
    a + 1.0;  // ::operator+(a, 1.0)
}

Operatory jednoargumentowe

Operator jednoargumentowy może być zdefiniowany jako

  • @a

    • niestatyczna metoda - aa.operator@()

    • globalna funkcja – operator@(aa)

  • a@

    • niestatyczna metoda - aa.operator@(int)

    • globalna funkcja – operator@(aa, int)

class X
{
public:
    X* operator&();  // operator pobrania adresu
    X operator&(X);  // dwuargumentowy X&X
};

X operator-(X); // jednoargumentowy minus

Operatory ++ i --

Operatory ++ i -- występują w postaci:

  • przedrostkowej (zwiększ i pobierz)

  • przyrostkowej (pobierz i zwiększ)

Problem z przeciążeniem - operatory te nie pobierają argumentu. Operatory w postaci przyrostkowej pobierają argument typu int (kompilatory w momencie wywołania przekazują 0 jako argument typu int).

Operatory te przekazują różne typy. Operator przedrostkowy przekazuje referencję. Operator przyrostkowy przekazuje obiekt z modyfikatorem const.

class Integer
{
    int value_ = 0;
public:
   Integer& operator++(); // przedrostkowy ++i
   const Integer operator++(int); // przyrostkowy i++

   Integer& operator+=(int);
};

Integer i;
++i; // wywołuje i.operator++();
i++; // wywołuje i.operator++(0);

Integer& Integer::operator++()
{
   *this += 1;  // zwiększ
   return *this;    // pobierz
}

const Integer Integer::operator++(int)
{
   Integer old_value = *this;
   ++(*this);
   return old_value;
}

int i;
++++i; // OK
i++++; // Błąd! i.operator++(0).operator++(0)

Operatory << i >>

Aby przeciążyć operatory << i >> dla typów definiowanych przez użytkownika należy zdefiniować globalne funkcje operatorowe. Jeżeli mają one mieć dostęp do prywatnych składowych klasy X należy umieścić w klasie deklarację przyjaźni z tymi funkcjami.

class X
{
    int value_ = 0;
public:
  friend std::ostream& operator<<(std::ostream&, const X&);
  friend std::istream& operator>>(std::istream&, const X&);
};

std::ostream& operator<<(std::ostream& out, const X& x)
{
    out << x.value_;

    return out;
}

std::istream& operator>>(std::istream& in, const X& x)
{
    in >> x.value_;

    return in;
}

// ...

X x;


std::cout << "X = " << x << std::endl;

Literały - operator ""

W C++11 możliwe jest definiowanie literałów dla typów definiowanych przez użytkownika przy pomocy funkcji operatora literalnego.

Nazwa operatora literalnego to operator "" wraz z odpowiednim sufiksem, np:

constexpr complex<double> operator"" i(long double d)
{
    return {0, d};
}

std::string operator"" s(const char* p, size_t n)
{
    return std::string{p, n};
}

Mechanizm tworzenia literałów użytkownika umożliwia zdefiniowanie nowego sufiksu dla literału, który go poprzedza. Istnieją cztery rodzaje literałów, które są akceptowane przy definicji literału użytkownika:

  • literał całkowity - unsigned long long lub const char*

  • literał zmiennoprzecinkowy - long double lub const char*

  • literał tekstowy - akceptujący parę argumentów (const char*, size_t)

  • typ znakowy char, wchar_t, char16_t i char32_t

Przykład 1:

Bignum operator"" x(const char* p)
{
    return Bignum(p);
}

void f(Bignum);

f(123456789012345678901234567890123456789012345x);

Przykład 2:

class Integer
{
    int value_ = 0;
public:
    constexpr Integer(int value = 0) : value_{ value }
    {}

    constexpr int value() const
    {
        return value_;
    }
};

constexpr Integer operator "" _i(unsigned long long x)
{
    return Integer{ static_cast<int>(x) };
}

// ...

auto x = 42_i;

assert(x.value() == 42);

Operatory rzutowania w C++

Klasyczne operatory rzutowania

Klasyczne rzutowanie typu

char* hopeItWorks = (char*)0x00ff0000;

Możliwy jest też inny sposób wyrażenia konwersji

typedef char* PChar;
hopeItWorks = Pchar(0x00ff0000); // rzutowanie ze składnią wywołania funkcji

Oba sposoby rzutowania nie są zalecane w C++

Ograniczone operatory rzutowania w C++

W C++ wprowadzono cztery nowe operatory rzutowania:

  • static_cast

  • const_cast

  • dynamic_cast

  • reinterpret_cast

Operator const_cast

Operator const_cast pozwala na dodawanie lub usuwanie z typu kwalifikatorów const i volatile.

const Person* get_employee()
{
    //...
}

//...
Person* anEmployee = const_cast<Person*>( get_employee() );

// odpowiednik klasycznego
anEmployee = (Person*)getEmployee();

Operator static_cast

Operator static_cast służy do rzutowania:

  • „w dół” hierarchii dziedziczenia – ze wskaźnika bądź referencji klasy bazowej na wskaźnik lub referencję klasy pochodnej (bez kontroli w czasie wykonania [run-time type check])

  • między typami liczbowymi (np. float na int)

class B {};
class D : public B {};

void f(B* pb, D* pd)
{
   D* pd2 = static_cast<D*>(pb); // niebezpieczne, pb może wskazywać na B
   B* pb2 = static_cast<B*>(pd); // bezpieczna konwersja
}

Operator dynamic_cast

Operator dynamic_cast służy do:

  • bezpiecznego rzutowania wskaźnika lub referencji klasy bazowej na wskaźnik lub referencję klasy pochodnej

  • rzutowanie można przeprowadzić jedynie dla typu polimorficznego

  • w przypadku nieudanego rzutowania wskaźników zwracany jest nullptr, a w przypadku referencji rzucany jest wyjątek std::bad_cast

// bezpieczna konwersja z kontrolą typów
const Circle* cp = dynamic_cast<const Circle*>(get_next_shape());

if (cp)
{
    std::cout << "r: " << cp->radius() << std::endl;
}

try
{
   const Circle& rc = dynamic_cast<const Circle&>(*get_next_shape());
}
catch(const std::bad_cast& e)
{
   //...
}

Operator reinterpret_cast

Operator reinterpret_cast pozwala na konwersję typu całkowitego na dowolny typ wskaźnikowy i odwrotnie. Rzutowania przy pomocy reinterpret_cast, zwykle nie będą przenośne między architekturami.

char* hope_it_works = reinterpret_cast<char*>(0x00ff0000);
int* hopeless = reinterpret_cast<int*>(hope_it_works);