struct S {
void f() {
std::cout << "Hi!";
}
};
int main() {
const S s;
.f(); // CE
s}
По умолчанию методы у классов считаются неконстантными. То есть
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 sconst S& r = s;
.f(); // 1
r}
Пример. Квадратные скобки для строки
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() {
= "abcd";
String s const String &cs = s;
const char &c = cs[0];
[0] = 'b';
s// 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 {
= y;
r }
};
Поскольку ссылка это по факту указатель, 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:
() { /* i.e open connection */ }
Singletonstatic Singleton* ptr = nullptr;
(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singletonpublic:
static Singleton& getObject() {
if (ptr == nullptr) {
= new Singleton();
ptr }
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*
operator""_bi(unsigned long long x) {
BigInteger return BigInteger(x);
};
1329_bi; // valid BigInteger
Стандартная строка также обладает литеральным суффиксом,
"abcdef"s
будет std::string
, а не
const char*
.
struct Complex {
double re = 0.0;
double im = 0.0;
(double re_) : re(re_) {}
Complex(double re_, double im_) : re(re_), im(im_) {}
Complex
operator+(const Complex &other) const {
Complex return Complex{re + other.re, im + other.im};
}
};
int main() {
(1.0);
Complex c+ 3.14; // OK, c.operator+(3.14)
c 3.14 + c; // CE, ???
}
Если нужно определить бинарный арифметический оператор, то лучше объявить его вне класса.
operator+(const Complex& a, const Complex& b) {
Complex return Complex(a.re + b.re, a.im + b.im);
}
operator+=
уже нужно определять внутри класса
& operator+=(const Complex &other) {
Complex*this = *this + other;
return *this;
}
Это очень плохой код, в том смысле, что он
неэффективный. Например, для строки это в два раза хуже, чем обычная
реализация. Лучше реализовать +
через +=
.
operator+(const Complex &a, const Complex &b) {
Complex = a;
Complex result += b;
result return result;
}
А не надо ли поставить const
?
int main() {
(1.0), b(2.0), c(3.0);
Complex a
+ b = c; // Why it compiles?
a }
Но слева же rvalue
. А с чего мы взяли, что нельзя
присваивать что-то rvalue
для нестандартных типов?
Один из способов решения это поставить const
перед
возвращаемым значением. Но это было актуально до C++-11, сейчас
так лучше не делать
struct Complex {
& operator=(const Complex& other) & {
Complex
} // now it can be applied only to lvalue
&operator=(const Complex &other) && {
Complex
} // only to rvalue
};
Внимание. В коде ниже происходит лишнее копирование
operator+(Complex a, const Complex &b) {
Complex 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>
.