3.5 Const, static and explicit methods

struct S {
    void f() {
        std::cout << "Hi!";
    }
};

int main() {
    const S s;
    s.f(); // CE
}

По умолчанию методы у классов считаются неконстантными. То есть S::f() не определена для const S, потому что она неконстанта. Константность нужно явно уточнить.

void f() const { // Now OK
    std::cout << "Hi!";
}

Можно делать перегрузку по признаку константности, ровно как можно делать перегрузку по константности аргумента, ведь объект класса это неявный “нулевой” аргумент.

struct S {
    void f() const {
        std::cout << 1;
    }
    void f() {
        std::cout << 2;
    }
};

int main() {
    S s;
    const S& r = s;
    r.f(); // 1
}

Пример. Квадратные скобки для строки

struct S {
    char arr[10];

    char& operator[](size_t index) { // for non-const access
        return arr[index];
    }

    const char& operator[](size_t index) const { // for const access
        return arr[index];
    }
};

Почему нельзя заменить const char& на char?

int main() {
    String s = "abcd";
    const String &cs = s;
    const char &c = cs[0];
    s[0] = 'b';
    // if operator[] returns const char&, c will be 'b', otherwise 'a'
}

Почему char& operator[](size_t index) const вообще скомпилируется? char *arr превращается в char* const arr, а не в const char* arr, поэтому при обращении к arr по индексу мы получаем неконстантную ссылку, поэтому всё компилируется.

К сожалению, следующий код компилируется

int x = 0;

struct S {
    int& r = x;

    void f(int y) const {
        r = y;
    }
};

Поскольку ссылка это по факту указатель, const на него не влияет.

А что если мы хотим менять поле у константного объекта? Для этого есть ключевое слово mutable.

struct S {
    mutable int n_calls = 0;

    void f() {
        ++n_calls;
        std::cout << "Hello!" << std::endl;
    }
};

Это даже может быть полезно, например, при реализации сплей-дерева в виде класса. Сплей-дерево после вызова find вызывает splay и перестраивает дерево. Но как быть, ведь find по-хорошему должен быть константным. Здесь как раз помогает ключевое слово mutable.

static методы – те, которые “относятся к классу в целом”. Для полей оно значит то же, что и для глобальных переменных.

struct S {
    static int x; // will be in static memory
    
    // Can be accessed via S::f();
    static void f() {
        std::cout << "Hi!" << std::endl;
    }
};

Классический пример – синглтон, класс, который должен существовать ровно один в программе, например, соединение с базой данных.

class Singleton {
private:
    Singleton() { /* i.e open connection */ }
    static Singleton* ptr = nullptr;

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static Singleton& getObject() {
        if (ptr == nullptr) {
            ptr = new Singleton();
        }
        return *ptr;
    }
};

Ключевое слово explicit запрещает вызывать методы неявно.

struct Latitude {
    double value;
    // Latitude(double value_) : value(value_) {}
    // f(Latitude) can be called by f(0.5) via implicit conversion
    // and we can confuse it with longitude
    explicit Latitude(double value_) : value(value_) {} // now implicit conversion is forbidden
};

А что если мы хотим приведение от Latitude к double?

operator double() const { // f(double) can be called via f(Latitude)
    return value;
}
// starting from C++-11 cast operators can be made explicit

В случае если оператор приведения был объявлен explicit можно воспользоваться static_cast.

Конструкторы из одного аргумента или операторы приведения типов лучше делать explicit, но не всегда.

Важный пример. Даже оператор конверсии BigInteger в bool нужно будет сделать explicit. Для if-ов добавили специальный костыль под названием contextual conversion, она происходит под if, while и тернарным оператором. Это особый вид конверсии, который разрешает рассматривать explicit конверсии.

Начиная с C++-11 можно определять свои литеральные суффиксы.

class BigInteger {};

// either unsigned long long, long double, const char*
BigInteger operator""_bi(unsigned long long x) {
    return BigInteger(x);
};

1329_bi; // valid BigInteger

Стандартная строка также обладает литеральным суффиксом, "abcdef"s будет std::string, а не const char*.

3.6 Operators overloading

struct Complex {
    double re = 0.0;
    double im = 0.0;
    Complex(double re_) : re(re_) {}
    Complex(double re_, double im_) : re(re_), im(im_) {}

    Complex operator+(const Complex &other) const {
        return Complex{re + other.re, im + other.im};
    }
};

int main() {
    Complex c(1.0);
    c + 3.14; // OK, c.operator+(3.14)
    3.14 + c; // CE, ???
}

Если нужно определить бинарный арифметический оператор, то лучше объявить его вне класса.

Complex operator+(const Complex& a, const Complex& b) {
    return Complex(a.re + b.re, a.im + b.im);
}

operator+= уже нужно определять внутри класса

Complex& operator+=(const Complex &other) {
    *this = *this + other;
    return *this;
}

Это очень плохой код, в том смысле, что он неэффективный. Например, для строки это в два раза хуже, чем обычная реализация. Лучше реализовать + через +=.

Complex operator+(const Complex &a, const Complex &b) {
    Complex result = a;
    result += b;
    return result;
}

А не надо ли поставить const?

int main() {
    Complex a(1.0), b(2.0), c(3.0);

    a + b = c; // Why it compiles?
}

Но слева же rvalue. А с чего мы взяли, что нельзя присваивать что-то rvalue для нестандартных типов?

Один из способов решения это поставить const перед возвращаемым значением. Но это было актуально до C++-11, сейчас так лучше не делать

struct Complex {
    Complex& operator=(const Complex& other) & {

    } // now it can be applied only to lvalue
    
    Complex &operator=(const Complex &other) && {

    } // only to rvalue
};

Внимание. В коде ниже происходит лишнее копирование

Complex operator+(Complex a, const Complex &b) {
    return a += b;
}

Так как компилятор в первом случае применит return value optimization, а здесь нет.

Можно также перегружать оператор вывода

std::ostream& operator<<(std::ostream &out, const String &str) {
    
}

Аналогично оператор ввода из потока

std::istream& operator>>(std::istream &in, String &str) {
    
}

Оператор ввода иногда разумно сделать friend.

Оператор возвращает тот же поток чтобы можно было использовать синтаксис std::cout << x << y.

Нельзя определить символ для обозначения оператора, приоритет оператора, порядок вычисления.

bool operator<(const Complex &a, const Complex &b) {
    return a.re < b.re || (a.re == b.re && a.im < b.im);
}

bool operator>(const Complex &a, const Complex &b) {
    return b < a; // same time as operator<
}

bool operator<=(const Complex &a, const Complex &b) {
    return !(a > b);
}

То есть желательно всё, кроме равенства выразить через operator<.

Начиная с C++-20 есть оператор “космический корабль”, также известный как three-way comparison.

??? operator<=>(const Complex &other) = default; // automatic deduction lexicographically

Возвращает один из трех типов std::weak_ordering, std::strong_ordering, std::partial_ordering.

std::partial_ordering: less, greater, equivalent, unordered.

Разница между std::strong_ordering, std::weak_ordering в том, когда достигается равенство. std::strong_ordering значит, что a == b => forall f: f(a) == f(b).

В строке, например, стандартный оператор <=> так как надо сравнивать не указатели, а значения под ними.

Все эти вещи определены в заголовочном файле <compare>.