13-bo‘lim

Operatorlar va qiymat semantikasi

O'z turingizni o'rnatilgan turdek ishlatish - solishtirish, arifmetika, indekslash va uch tomonlama operator.

🕑 13 daqiqa o‘qish 📄 619 so‘z 👁 2 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Muammo: yalang'och son
  2. Arifmetik operatorlar
  3. Uch tomonlama solishtirish
  4. Qo'lda yozilgan <=>
  5. Indekslash va chaqiruv
  6. Oqimga chiqarish va std::formatter
  7. Nima yuklash kerak emas
  8. Xulosa

C++ da int maxsus tur emas. Uning barcha imkoniyatlarini o'z turingizga ham berish mumkin - va shu bilan "aylanma" (Aylanma) yoki "pul" (Pul) kabi tushunchalar kodda tabiiy ko'rinadi.

Muammo: yalang'och son #

C++
#include <print>
#include <string>
#include <vector>
#include <compare>
#include <format>
#include <stdexcept>
#include <algorithm>
C++
// Yalang'och sonlar - hech narsa xato qilishga to'sqinlik qilmaydi
long long summa(long long sum, long long dollar)
{
    return sum + dollar;          // MA'NOSIZ, lekin kompilyatsiya qilinadi
}

int main()
{
    long long narx_sum = 150'000;
    long long narx_dollar = 12;

    std::println("bema'ni qo'shuv: {}", summa(narx_sum, narx_dollar));
    std::println("kompilyator hech nima demadi.");

    std::println("");
    std::println("Yana bir muammo - vergul qayerda:");
    long long tiyin = 15'000'050;
    std::println("  xom son:  {}", tiyin);
    std::println("  har safar qo'lda formatlash kerak");

    return 0;
}
Natija
bema'ni qo'shuv: 150012
kompilyator hech nima demadi.

Yana bir muammo - vergul qayerda:
  xom son:  15000050
  har safar qo'lda formatlash kerak
Kuchli turlar - xatoni kompilyatsiya paytiga surish

Pulni long long bilan ifodalash uch muammo tug'diradi:

MuammoNatija
Valyuta yo'qSo'm dollarga qo'shiladi
Birlik yo'qSo'mmi, tiyinmi - noma'lum
Formatlash tarqoqHar joyda qaytariladi

Yechim - Pul turini yaratish. U ichida hali ham long long saqlaydi, lekin qoidalarni o'zi biladi.

Bu qo'shimcha ishlash xarajati nolga teng: optimallashtiruvchi kompilyator bunday o'ramni butunlay yo'q qiladi va mashina kodida oddiy long long qoladi.

Arifmetik operatorlar #

C++
class Pul
{
    long long tiyin_{};          // eng kichik birlikda saqlaymiz

public:
    Pul() = default;

    static Pul somdan(long long som, int tiyin = 0)
    {
        Pul p;
        p.tiyin_ = som * 100 + tiyin;
        return p;
    }

    long long tiyin() const { return tiyin_; }

    Pul& operator+=(const Pul& b) { tiyin_ += b.tiyin_; return *this; }
    Pul& operator-=(const Pul& b) { tiyin_ -= b.tiyin_; return *this; }
    Pul& operator*=(int n)        { tiyin_ *= n;        return *this; }

    std::string matn() const
    {
        long long musbat = tiyin_ < 0 ? -tiyin_ : tiyin_;
        const char* ishora = tiyin_ < 0 ? "-" : "";
        return std::format("{}{}.{:02} so'm", ishora, musbat / 100, musbat % 100);
    }
};

// Ikki argumentli operatorlar - erkin funksiya
Pul operator+(Pul a, const Pul& b) { a += b; return a; }
Pul operator-(Pul a, const Pul& b) { a -= b; return a; }
Pul operator*(Pul a, int n)        { a *= n; return a; }
Pul operator*(int n, Pul a)        { a *= n; return a; }

int main()
{
    Pul non = Pul::somdan(4'000);
    Pul sut = Pul::somdan(12'500, 50);

    std::println("non: {}", non.matn());
    std::println("sut: {}", sut.matn());

    Pul jami = non + sut;
    std::println("jami: {}", jami.matn());

    Pul uchta = non * 3;
    std::println("uchta non: {}", uchta.matn());

    Pul teskari = 2 * sut;
    std::println("ikki sut: {}", teskari.matn());

    Pul qarz = non - sut;
    std::println("farq: {}", qarz.matn());

    std::println("");
    // Pul + 5   ← kompilyatsiya xatosi. Aynan shuni xohlagandik.

    return 0;
}
Natija
non: 4000.00 so'm
sut: 12500.50 so'm
jami: 16500.50 so'm
uchta non: 12000.00 so'm
ikki sut: 25001.00 so'm
farq: -8500.50 so'm
+= ni a'zo, + ni erkin funksiya qiling

Bu naqsh sabablari bor:

+= a'zo bo'lishi kerak, chunki u chap tomonni o'zgartiradi va this ga kirish talab qiladi.

+ erkin bo'lishi kerak, chunki u nosimmetrik bo'lishi mumkin emas. Agar + a'zo bo'lganida:

KOD
Pul p = pul + 5;      // ishlaydi (agar aylantirish bo'lsa)
Pul q = 5 + pul;      // ISHLAMAYDI - 5 da a'zo funksiya yo'q

Erkin operator+ esa ikkala tomonni teng ko'radi.

Diqqat: operator+ birinchi argumentni qiymat bo'yicha oladi:

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

Bu ataylab: a allaqachon nusxa, uni o'zgartirib qaytaramiz. Vaqtinchalik obyekt uzatilsa - u ko'chiriladi, qo'shimcha nusxa bo'lmaydi.

Uch tomonlama solishtirish #

C++
class Versiya
{
    int katta_{};
    int orta_{};
    int kichik_{};

public:
    Versiya(int a, int b, int c) : katta_(a), orta_(b), kichik_(c) {}

    // Bitta qator - oltita operator
    auto operator<=>(const Versiya&) const = default;
    bool operator==(const Versiya&) const = default;

    std::string matn() const
    {
        return std::format("{}.{}.{}", katta_, orta_, kichik_);
    }
};

int main()
{
    Versiya a{1, 4, 0};
    Versiya b{1, 10, 0};
    Versiya c{1, 4, 0};

    std::println("a = {}", a.matn());
    std::println("b = {}", b.matn());
    std::println("c = {}", c.matn());

    std::println("");
    std::println("a <  b : {}", a < b);
    std::println("a >  b : {}", a > b);
    std::println("a == c : {}", a == c);
    std::println("a != b : {}", a != b);
    std::println("a <= c : {}", a <= c);

    std::println("");
    std::vector<Versiya> ro = {{2, 0, 0}, {1, 4, 0}, {1, 10, 3}, {1, 4, 7}};
    std::ranges::sort(ro);

    std::print("saralangan:");
    for (const auto& v : ro) std::print(" {}", v.matn());
    std::println("");

    return 0;
}
Natija
a = 1.4.0
b = 1.10.0
c = 1.4.0

a <  b : true
a >  b : false
a == c : true
a != b : true
a <= c : true

saralangan: 1.4.0 1.4.7 1.10.3 2.0.0
= default bo'lgan <=> a'zolarni tartib bilan solishtiradi

auto operator<=>(const Versiya&) const = default; bitta qatorda <, >, <=, >= ni beradi. Ikkinchi qator == va != ni beradi.

Kompilyator a'zolarni e'lon tartibida solishtiradi: avval katta_, teng bo'lsa orta_, keyin kichik_. Bu aynan versiya raqamlari uchun kerakli tartib - lekin a'zolar tartibiga bog'liqligini unutmang.

C++20 gacha oltita operatorni qo'lda yozish kerak edi - va ular orasida nomuvofiqlik keng tarqalgan xato edi.

Uchta qaytish turi bor:

TurMa'nosi
std::strong_orderingTeng qiymatlar almashtiriladi
std::weak_orderingTeng, lekin farqi bor
std::partial_orderingSolishtirib bo'lmaydigan juftlik bor

double uchun partial_ordering - chunki NaN hech nima bilan solishtirilmaydi.

Qo'lda yozilgan <=> #

C++
class Talaba
{
    std::string ism_;
    double ball_{};

public:
    Talaba(std::string ism, double ball)
        : ism_(std::move(ism)), ball_(ball) {}

    // Avval ballga ko'ra (kamayish), keyin ismga ko'ra
    std::weak_ordering operator<=>(const Talaba& b) const
    {
        if (ball_ != b.ball_) {
            return ball_ > b.ball_ ? std::weak_ordering::less
                                   : std::weak_ordering::greater;
        }
        return ism_ <=> b.ism_;      // strong_ordering -> weak_ordering
    }

    bool operator==(const Talaba& b) const
    {
        return ball_ == b.ball_ && ism_ == b.ism_;
    }

    const std::string& ism() const { return ism_; }
    double ball() const { return ball_; }
};

int main()
{
    std::vector<Talaba> guruh = {
        {"Nodira",   88.5},
        {"Husanboy", 94.0},
        {"Aziza",    94.0},
        {"Kamola",   76.25},
        {"Malika",   88.5},
    };

    std::ranges::sort(guruh);

    std::println("{:<10} {}", "ISM", "BALL");
    std::println("{}", std::string(20, '-'));
    for (const auto& t : guruh) {
        std::println("{:<10} {:.2f}", t.ism(), t.ball());
    }

    std::println("");
    std::println("eng yuqori: {}", guruh.front().ism());
    std::println("eng past:   {}", guruh.back().ism());

    return 0;
}
Natija
ISM        BALL
--------------------
Aziza      94.00
Husanboy   94.00
Malika     88.50
Nodira     88.50
Kamola     76.25

eng yuqori: Aziza
eng past:   Kamola
double uchun <=> weak_ordering bermaydi

Bu funksiyani avval shunday yozgan edik:

KOD
if (auto c = b.ball_ <=> ball_; c != 0) return c;

Kompilyator rad etdi. Sabab: double <=> double std::partial_ordering qaytaradi (chunki NaN mavjud), va u weak_ordering ga aylanmaydi - faqat teskarisi mumkin.

Aylanish yo'nalishi bir tomonlama:

ManbaNimaga aylanadi
strong_orderingweak_ordering, partial_ordering
weak_orderingpartial_ordering
partial_orderingHech nimaga

Shuning uchun double maydonini qo'lda solishtirdik va std::weak_ordering::less / ::greater ni qaytardik. Ikkinchi qadamdagi ism_ <=> b.ism_ esa strong_ordering beradi va u weak_ordering ga o'zi aylanadi.

Muqobil yo'l - butun funksiyani partial_ordering qaytaradigan qilish, lekin unda NaN ball saralashni buzadi.

Operatorni qayerga yozish A'ZO FUNKSIYA Chap tomonni o'zgartiradi yoki holatga kiradi operator= tayinlash operator+= -= *= /= joyida o'zgartirish operator[] indekslash operator() chaqiruv operator-> * ko'rsatkichdek operator++ -- oshirish ERKIN FUNKSIYA Ikki tomon teng - simmetriya kerak operator+ - * / arifmetika operator== != tenglik operator<=> tartib operator<< >> oqimga chiqarish Sabab: 5 + pul ham ishlashi kerak. A'zo bo'lsa - faqat pul + 5 ishlaydi. Standart naqsh Pul& operator+=(const Pul& b); // a'zo - asosiy mantiq Pul operator+(Pul a, const Pul& b) { a += b; return a; } // erkin Ortiqcha yuklamang Operator ma'nosi kutilganday bo'lsin. + qo'shsin, == tenglikni tekshirsin. Aqlli, lekin g'alati operator - nomli funksiyadan yomonroq.
Chap tomonga kirish kerakmi - a'zo; simmetriya kerakmi - erkin

Indekslash va chaqiruv #

C++
class Matritsa
{
    std::size_t satr_{};
    std::size_t ustun_{};
    std::vector<double> ma_;

public:
    Matritsa(std::size_t s, std::size_t u)
        : satr_(s), ustun_(u), ma_(s * u, 0.0) {}

    // Ikki indeksli kirish - operator()
    double& operator()(std::size_t s, std::size_t u)
    {
        if (s >= satr_ || u >= ustun_) throw std::out_of_range{"chegaradan tashqari"};
        return ma_[s * ustun_ + u];
    }

    double operator()(std::size_t s, std::size_t u) const
    {
        if (s >= satr_ || u >= ustun_) throw std::out_of_range{"chegaradan tashqari"};
        return ma_[s * ustun_ + u];
    }

    std::size_t satrlar() const { return satr_; }
    std::size_t ustunlar() const { return ustun_; }

    void chop(const std::string& nom) const
    {
        std::println("{}:", nom);
        for (std::size_t s = 0; s < satr_; ++s) {
            std::string qator;
            for (std::size_t u = 0; u < ustun_; ++u) {
                qator += std::format("{:7.2f}", (*this)(s, u));
            }
            std::println("  {}", qator);
        }
    }
};

Matritsa operator*(const Matritsa& a, const Matritsa& b)
{
    if (a.ustunlar() != b.satrlar()) throw std::invalid_argument{"o'lcham mos emas"};

    Matritsa n{a.satrlar(), b.ustunlar()};
    for (std::size_t s = 0; s < a.satrlar(); ++s) {
        for (std::size_t u = 0; u < b.ustunlar(); ++u) {
            double jami = 0;
            for (std::size_t k = 0; k < a.ustunlar(); ++k) jami += a(s, k) * b(k, u);
            n(s, u) = jami;
        }
    }
    return n;
}

int main()
{
    Matritsa a{2, 3};
    a(0,0) = 1; a(0,1) = 2; a(0,2) = 3;
    a(1,0) = 4; a(1,1) = 5; a(1,2) = 6;

    Matritsa b{3, 2};
    b(0,0) = 7;  b(0,1) = 8;
    b(1,0) = 9;  b(1,1) = 10;
    b(2,0) = 11; b(2,1) = 12;

    a.chop("A");
    b.chop("B");

    Matritsa c = a * b;
    c.chop("A * B");

    std::println("");
    try {
        a(5, 0) = 1;
    } catch (const std::out_of_range& x) {
        std::println("xato tutildi: {}", x.what());
    }

    return 0;
}
Natija
A:
     1.00   2.00   3.00
     4.00   5.00   6.00
B:
     7.00   8.00
     9.00  10.00
    11.00  12.00
A * B:
    58.00  64.00
   139.00 154.00

xato tutildi: chegaradan tashqari
operator[] va operator() - chegarani tekshirasizmi

Standart konteynerlarda operator[] tekshirmaydi (tez), at() esa tekshiradi (xavfsiz).

O'z turingizda tanlov sizniki, lekin izchil bo'ling va hujjatlashtiring.

C++23 dan boshlab operator[] bir nechta argument qabul qila oladi:

KOD
double& operator[](std::size_t s, std::size_t u);   // C++23
m[1, 2] = 5.0;

Undan oldin operator() yagona yo'l edi. Yangi kodda operator[] ni ishlating - u ko'proq "indeks" ma'nosini beradi.

Ikki versiya (const va const bo'lmagan) yozish kerakligiga e'tibor bering - biri o'qish, biri yozish uchun.

Oqimga chiqarish va std::formatter #

C++
struct Nuqta
{
    double x{};
    double y{};
};

// std::format uchun o'z turimizni ro'yxatdan o'tkazamiz
template <>
struct std::formatter<Nuqta>
{
    constexpr auto parse(std::format_parse_context& c) { return c.begin(); }

    auto format(const Nuqta& n, std::format_context& c) const
    {
        return std::format_to(c.out(), "({:.2f}, {:.2f})", n.x, n.y);
    }
};

Nuqta operator+(const Nuqta& a, const Nuqta& b) { return {a.x + b.x, a.y + b.y}; }
Nuqta operator*(const Nuqta& a, double k)       { return {a.x * k, a.y * k}; }

int main()
{
    Nuqta a{3.0, 4.0};
    Nuqta b{1.5, -2.0};

    std::println("a       = {}", a);
    std::println("b       = {}", b);
    std::println("a + b   = {}", a + b);
    std::println("a * 2   = {}", a * 2.0);

    std::println("");
    std::vector<Nuqta> yol = {{0,0}, {1,1}, {3,2}, {3,5}};
    for (std::size_t i = 0; i < yol.size(); ++i) {
        std::println("  {}-nuqta: {}", i, yol[i]);
    }

    return 0;
}
Natija
a       = (3.00, 4.00)
b       = (1.50, -2.00)
a + b   = (4.50, 2.00)
a * 2   = (6.00, 8.00)

  0-nuqta: (0.00, 0.00)
  1-nuqta: (1.00, 1.00)
  2-nuqta: (3.00, 2.00)
  3-nuqta: (3.00, 5.00)
std::formatter - zamonaviy operator<<

Eski C++ da chiqarish uchun operator<< yozilardi:

KOD
std::ostream& operator<<(std::ostream& o, const Nuqta& n);

U hali ham ishlaydi va iostream bilan kerak. Lekin std::print va std::format uchun std::formatter ixtisoslashtirish kerak.

Ustunliklari:

formatteroperator<<
Formatlash spetsifikatorlariYo'q
Tur xavfsiz, kompilyatsiyada tekshiriladiIsh paytida
Lokal va oqim holatidan mustaqilHolatga bog'liq
TezroqSekinroq

parse funksiyasi format satridagi {:...} ichini o'qiydi. Bizning misolimizda uni e'tiborsiz qoldirdik - c.begin() qaytarish "hech qanday spetsifikator qabul qilmayman" degani.

Nima yuklash kerak emas #

C++
int main()
{
    std::println("{:<24} {}", "OPERATOR", "MASLAHAT");
    std::println("{}", std::string(58, '-'));
    std::println("{:<24} {}", "&&  ||", "YUKLAMANG - qisqa hisob yo'qoladi");
    std::println("{:<24} {}", ",", "YUKLAMANG - tartib yo'qoladi");
    std::println("{:<24} {}", "&  (manzil olish)", "YUKLAMANG - hamma chalg'iydi");
    std::println("{:<24} {}", "->", "Faqat aqlli ko'rsatkichda");
    std::println("{:<24} {}", "()", "Faqat funksiya-obyektda");
    std::println("{:<24} {}", "new  delete", "Faqat ajratgich yozayotganda");
    std::println("{:<24} {}", "+ - * /", "Faqat MATEMATIK ma'noda");
    std::println("{:<24} {}", "<=>  ==", "Deyarli har doim = default");

    std::println("");
    std::println("Oltin qoida:");
    std::println("  Operator ma'nosini o'zgartirmang.");
    std::println("  Shubha bo'lsa - nomli funksiya yozing.");
    std::println("  qoshildi() - g'alati operator+ dan ravshanroq.");

    return 0;
}
Natija
OPERATOR                 MASLAHAT
----------------------------------------------------------
&&  ||                   YUKLAMANG - qisqa hisob yo'qoladi
,                        YUKLAMANG - tartib yo'qoladi
&  (manzil olish)        YUKLAMANG - hamma chalg'iydi
->                       Faqat aqlli ko'rsatkichda
()                       Faqat funksiya-obyektda
new  delete              Faqat ajratgich yozayotganda
+ - * /                  Faqat MATEMATIK ma'noda
<=>  ==                  Deyarli har doim = default

Oltin qoida:
  Operator ma'nosini o'zgartirmang.
  Shubha bo'lsa - nomli funksiya yozing.
  qoshildi() - g'alati operator+ dan ravshanroq.
&& va || ni yuklamang

O'rnatilgan && va || qisqa hisob qiladi: chap tomon javobni aniqlasa, o'ng tomon umuman hisoblanmaydi.

KOD
if (p != nullptr && p->qiymat > 0)     // p null bo'lsa - xavfsiz

Ortiqcha yuklangan operator&& esa oddiy funksiya - u ikkala argumentni hisoblab, keyin chaqiriladi. Yuqoridagi kod nol ko'rsatkichni ochib yuborardi.

Bundan tashqari hisoblash tartibi ham kafolatlanmagan.

Xuddi shu sabab operator, uchun ham amal qiladi.

Bu operatorlarni yuklash texnik jihatdan mumkin, lekin natija o'quvchini aldaydi - va aldash eng qimmat xato turi.

Amaliy topshiriq
  1. Pulni long long bilan ifodalab, so'm va dollarni qo'shib ko'ring.
  2. Pul turini yarating va += ni a'zo qilib yozing.
  3. operator+ ni erkin funksiya sifatida qo'shing.
  4. 5 * pul ishlashi uchun ikkinchi ortiqcha yuk yozing.
  5. auto operator<=>(...) const = default; bilan turni saralang.
  6. A'zolar tartibini almashtiring - saralash o'zgardimi?
  7. Qo'lda <=> yozib teskari tartibda saralang.
  8. operator() bilan ikki indeksli matritsa yozing.
  9. const va const bo'lmagan ikki versiyani qo'shing.
  10. O'z turingiz uchun std::formatter ixtisoslashtiring.

Xulosa #

  • Kuchli turlar xatoni kompilyatsiya paytiga suradi va xarajati nolga teng.
  • Chap tomonni o'zgartiradigan operatorlar (=, +=, [], ()) - a'zo.
  • Simmetriya kerak bo'lganlar (+, ==, <=>) - erkin funksiya.
  • Standart naqsh: += da mantiq, + esa uni chaqiradi.
  • auto operator<=>(...) const = default bitta qatorda to'rt solishtirishni beradi.
  • U a'zolarni e'lon tartibida solishtiradi - tartibga e'tibor bering.
  • == ni alohida = default qilish kerak.
  • Qo'lda yozilgan <=> da if (auto c = ...; c != 0) return c; naqshini ishlating.
  • Ko'p indeksli kirish uchun operator() yoki C++23 da ko'p argumentli operator[].
  • std::formatter ixtisoslashtirish - std::print uchun zamonaviy yo'l.
  • &&, ||, , va & ni hech qachon yuklamang.

Keyingi bo'limda vorislik va polimorfizm ni ko'ramiz - va ular qachon kerak emasligini.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.