10 KiB
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>
.