C++

Приведення типів, explicit та delete

Оператори приведення типів (user-defined conversions), конструктори-конвертери, ключові слова explicit та delete. Захист від неявних конвертацій.

Приведення типів, explicit та delete

Невидимі конвертації: коли компілятор занадто «розумний»

У попередніх статтях ми навчили наші класи працювати з операторами: додавання, множення, порівняння. Код виглядає природно і читабельно. Але C++ має ще одну потужну — і водночас небезпечну — можливість: автоматичне приведення типів (implicit type conversion).

Розглянемо просту ситуацію:

ImplicitConversion.cpp
#include <iostream>

class Dollars
{
private:
    int amount;

public:
    Dollars(int a = 0) : amount(a) {}

    int getAmount() const { return amount; }

    void print() const
    {
        std::cout << "$" << amount;
    }
};

void printMoney(Dollars money)
{
    money.print();
    std::cout << "\n";
}

int main()
{
    Dollars wallet(50);
    printMoney(wallet);  // ✅ Очевидно: передаємо об'єкт Dollars

    // ❓ А це?
    printMoney(100);     // ✅ Компілюється! Чому?
    printMoney(3.14);    // ✅ Теж працює! double → int → Dollars

    return 0;
}
./ImplicitConversion
$ ./ImplicitConversion
$50
$100
$3
Execution finished with exit code 0.

Що відбулося? На рядку 32 компілятор бачить printMoney(100) і розуміє: функція очікує Dollars, але отримує int. Компілятор знаходить конструктор Dollars(int a) і автоматично створює тимчасовий об'єкт Dollars(100). Це працює, але чи завжди це бажано?

Рядок 33: 3.14 → спочатку double перетворюється на int (стандартна конвертація), потім int перетворюється на Dollars (користувацька конвертація). Втрачається частина числа.

Такі неявні конвертації можуть призвести до тонких помилок, коли компілятор «здогадується» неправильно, і код компілюється без попереджень, але працює не так, як очікувалося.

У цій статті ми навчимося контролювати ці конвертації: дозволяти лише явні, забороняти небезпечні, створювати власні оператори приведення типів.

Термінологія:
  • Implicit conversion (неявна конвертація) — компілятор автоматично перетворює один тип на інший
  • Explicit conversion (явна конвертація) — програміст явно вказує приведення типу через static_cast або викликає конструктор
  • User-defined conversion (користувацька конвертація) — конвертація, визначена програмістом через конструктори або оператори приведення

Конструктори як оператори конвертації

Будь-який конструктор, що приймає один аргумент, компілятор розглядає як оператор неявної конвертації. Це означає, що коли компілятор бачить невідповідність типів, він автоматично викликає такий конструктор.

ConvertingConstructor.cpp
#include <iostream>

class Fraction
{
private:
    int numerator;
    int denominator;

public:
    // Конструктор із одним параметром — оператор конвертації int → Fraction
    Fraction(int num) : numerator(num), denominator(1)
    {
        std::cout << "Converting " << num << " to Fraction\n";
    }

    Fraction(int num, int den) : numerator(num), denominator(den) {}

    void print() const
    {
        std::cout << numerator << "/" << denominator;
    }
};

void printFraction(Fraction f)
{
    f.print();
    std::cout << "\n";
}

int main()
{
    Fraction f1(3, 4);
    printFraction(f1);      // ✅ Прямий виклик

    // Неявна конвертація int → Fraction
    printFraction(5);       // ⚠️ Створюється Fraction(5, 1)

    // Неявна конвертація у виразах
    Fraction f2 = 7;        // ⚠️ Еквівалентно Fraction f2(7);

    return 0;
}
./ConvertingConstructor
$ ./ConvertingConstructor
3/4
Converting 5 to Fraction
5/1
Converting 7 to Fraction
Execution finished with exit code 0.

Рядок 11: конструктор Fraction(int num) приймає один параметр. Компілятор дозволяє використовувати його для автоматичної конвертації intFraction.

Рядок 36: printFraction(5) — компілятор бачить невідповідність: функція очікує Fraction, але отримує int. Компілятор викликає Fraction(5), створює тимчасовий об'єкт Fraction(5, 1), передає його у функцію.

Рядок 39: Fraction f2 = 7 — не викликається operator=, бо об'єкт ще не існує. Це ініціалізація, що еквівалентна Fraction f2(7). Компілятор викликає конвертуючий конструктор.

Небезпека неявних конвертацій: уявімо функцію void processAccount(BankAccount acc). Якщо у класі BankAccount є конструктор BankAccount(int accountNumber), то виклик processAccount(12345) компілюється без помилок. Але програміст міг мати на увазі щось інше (наприклад, передати суму), і код працює некоректно. Компілятор не попередить.

Ключове слово explicit: заборона неявних конвертацій

Щоб заборонити компілятору автоматично викликати конструктор для конвертації, використовуємо ключове слово explicit. Воно дозволяє лише явні виклики конструктора.

ExplicitConstructor.cpp
#include <iostream>

class Fraction
{
private:
    int numerator;
    int denominator;

public:
    // explicit забороняє неявну конвертацію int → Fraction
    explicit Fraction(int num) : numerator(num), denominator(1)
    {
        std::cout << "Explicit constructor called\n";
    }

    Fraction(int num, int den) : numerator(num), denominator(den) {}

    void print() const
    {
        std::cout << numerator << "/" << denominator;
    }
};

void printFraction(Fraction f)
{
    f.print();
    std::cout << "\n";
}

int main()
{
    Fraction f1(3, 4);
    printFraction(f1);      // ✅ Працює

    // ❌ Неявна конвертація заборонена
    // printFraction(5);    // ПОМИЛКА КОМПІЛЯЦІЇ!

    // ✅ Явна конвертація дозволена
    printFraction(Fraction(5));      // Явний виклик конструктора
    printFraction(static_cast<Fraction>(7));  // Явне приведення

    // ❌ Неявна ініціалізація заборонена
    // Fraction f2 = 9;     // ПОМИЛКА КОМПІЛЯЦІЇ!

    // ✅ Явна ініціалізація дозволена
    Fraction f3(9);         // Пряма ініціалізація
    Fraction f4{11};        // Uniform initialization

    return 0;
}
./ExplicitConstructor
$ ./ExplicitConstructor
3/4
Explicit constructor called
5/1
Explicit constructor called
7/1
Explicit constructor called
Explicit constructor called
Execution finished with exit code 0.

Рядок 11: ключове слово explicit перед конструктором забороняє неявні конвертації. Компілятор не дозволить автоматичний виклик цього конструктора.

Рядок 36 (закоментований): printFraction(5) тепер помилка компіляції. Компілятор не може автоматично створити Fraction із int.

Рядок 39–40: явний виклик конструктора Fraction(5) або явне приведення static_cast<Fraction>(7) дозволені. Програміст явно вказує намір.

Рядок 43 (закоментований): Fraction f2 = 9 — помилка компіляції. Синтаксис із = інтерпретується як неявна конвертація.

Рядки 46–47: пряма ініціалізація Fraction f3(9) або uniform initialization Fraction f4{11} працюють, бо це явні виклики конструктора.

Правило безпеки: якщо конструктор приймає один аргумент (або всі інші аргументи мають значення за замовчуванням), завжди робіть його explicit, якщо неявна конвертація не є очевидною і бажаною. Це захищає від випадкових помилок. Виключення: типи-обгортки на зразок std::string, де std::string s = "hello"; є природним.

Оператори приведення типів: user-defined conversions

Окрім конвертації в наш клас (через конструктори), C++ дозволяє визначити конвертацію з нашого класу в інші типи. Це робиться через оператори приведення типів (conversion operators).

Синтаксис:

operator ТипПризначення() const
{
    // Конвертуємо поточний об'єкт у ТипПризначення
    return значення;
}

Особливості:

  • Немає типу повернення (тип вказано в імені оператора)
  • Зазвичай const-метод (не змінює об'єкт)
  • Компілятор викликає автоматично при невідповідності типів
ConversionOperator.cpp
#include <iostream>

class Fraction
{
private:
    int numerator;
    int denominator;

public:
    Fraction(int num, int den) : numerator(num), denominator(den) {}

    // Оператор конвертації Fraction → double
    operator double() const
    {
        return static_cast<double>(numerator) / denominator;
    }

    // Оператор конвертації Fraction → int (повертає цілу частину)
    operator int() const
    {
        return numerator / denominator;
    }

    void print() const
    {
        std::cout << numerator << "/" << denominator;
    }
};

void printDouble(double value)
{
    std::cout << "Double: " << value << "\n";
}

void printInt(int value)
{
    std::cout << "Int: " << value << "\n";
}

int main()
{
    Fraction f(7, 2);  // 7/2 = 3.5

    std::cout << "Fraction: ";
    f.print();
    std::cout << "\n\n";

    // Неявна конвертація Fraction → double
    printDouble(f);    // Викликається operator double()

    // Неявна конвертація Fraction → int
    printInt(f);       // Викликається operator int()

    // Явна конвертація через static_cast
    double d = static_cast<double>(f);
    int i = static_cast<int>(f);

    std::cout << "\nExplicit conversions:\n";
    std::cout << "Double: " << d << "\n";
    std::cout << "Int: " << i << "\n";

    // Неявна конвертація у виразах
    double result = f + 1.5;  // f → double, потім 3.5 + 1.5
    std::cout << "\nf + 1.5 = " << result << "\n";

    return 0;
}
./ConversionOperator
$ ./ConversionOperator
Fraction: 7/2
Double: 3.5
Int: 3
Explicit conversions:
Double: 3.5
Int: 3
f + 1.5 = 5
Execution finished with exit code 0.

Рядки 13–16: operator double() перетворює дріб на double, обчислюючи ділення numerator / denominator. Компілятор викликає цей оператор автоматично, коли очікується double, але передається Fraction.

Рядки 19–22: operator int() повертає цілу частину дробу (округлення вниз через цілочисельне ділення).

Рядок 51: printDouble(f) — компілятор бачить невідповідність типів: функція очікує double, але отримує Fraction. Компілятор знаходить operator double() і викликає його.

Рядок 66: вираз f + 1.5 — компілятор перетворює f на double (через operator double()), потім виконує 3.5 + 1.5 = 5.0.

Небезпека багатозначності: якщо у класу є і operator int(), і operator double(), деякі вирази можуть бути неоднозначними. Наприклад, f + 1 — компілятор не знає, чи конвертувати f в int або double. Це викличе помилку компіляції: ambiguous overload. Щоб уникнути цього, використовуйте оператори приведення обережно.

explicit для операторів приведення

Так само, як для конструкторів, ключове слово explicit можна застосовувати до операторів приведення типів. Це забороняє неявну конвертацію, дозволяючи лише явну.

ExplicitConversion.cpp
#include <iostream>

class Fraction
{
private:
    int numerator;
    int denominator;

public:
    Fraction(int num, int den) : numerator(num), denominator(den) {}

    // explicit забороняє неявну конвертацію Fraction → double
    explicit operator double() const
    {
        return static_cast<double>(numerator) / denominator;
    }

    void print() const
    {
        std::cout << numerator << "/" << denominator;
    }
};

void printDouble(double value)
{
    std::cout << "Double: " << value << "\n";
}

int main()
{
    Fraction f(3, 4);

    // ❌ Неявна конвертація заборонена
    // printDouble(f);        // ПОМИЛКА КОМПІЛЯЦІЇ!
    // double d = f;          // ПОМИЛКА КОМПІЛЯЦІЇ!
    // double sum = f + 1.5;  // ПОМИЛКА КОМПІЛЯЦІЇ!

    // ✅ Явна конвертація дозволена
    printDouble(static_cast<double>(f));
    double d = static_cast<double>(f);
    std::cout << "Explicit conversion: " << d << "\n";

    return 0;
}
./ExplicitConversion
$ ./ExplicitConversion
Double: 0.75
Explicit conversion: 0.75
Execution finished with exit code 0.

Рядок 13: explicit operator double() дозволяє конвертацію лише через явний static_cast або пряме приведення.

Рядки 34–36 (закоментовані): всі неявні конвертації тепер заборонені. Компілятор видасть помилку: cannot convert 'Fraction' to 'double' without a explicit conversion.

Рядки 39–40: явне приведення static_cast<double>(f) працює. Програміст явно вказує намір конвертувати.


Спеціальний випадок: explicit operator bool()

Один із найпоширеніших патернів — конвертація об'єкта в bool для використання в умовах (if, while). Наприклад, std::ifstream можна перевіряти: if (file) { ... }.

Але operator bool() без explicit може призвести до дуже дивних помилок:

BoolConversionProblem.cpp
#include <iostream>

class Pointer
{
private:
    int* ptr;

public:
    Pointer(int* p = nullptr) : ptr(p) {}

    // НЕ explicit — небезпечно!
    operator bool() const
    {
        return ptr != nullptr;
    }
};

int main()
{
    Pointer p1(new int(10));
    Pointer p2(nullptr);

    // ✅ Передбачувана поведінка
    if (p1)
    {
        std::cout << "p1 is not null\n";
    }

    // ❌ Несподівана поведінка: компілюється, але помилка!
    int sum = p1 + p2;  // p1 → bool → int (1), p2 → bool → int (0), sum = 1
    std::cout << "sum = " << sum << "\n";  // sum = 1 — що???

    // ❌ Ще гірше: можна порівнювати з числами!
    if (p1 == 1)  // p1 → bool → int, порівнюємо int == int
    {
        std::cout << "p1 == 1 ?!\n";
    }

    return 0;
}
./BoolConversionProblem
$ ./BoolConversionProblem
p1 is not null
sum = 1
p1 == 1 ?!
Execution finished with exit code 0.

Рядок 31: p1 + p2 — компілятор перетворює p1 і p2 на bool (через operator bool()), потім bool на int (стандартна конвертація true1, false0), потім додає їх. Результат: 1 + 0 = 1. Це абсурд, але компілюється без помилок!

Рядок 35: p1 == 1 — компілятор перетворює p1 на bool, потім на int, порівнює 1 == 1. Працює, але семантично неправильно.

Рішення: використовуйте explicit operator bool():

ExplicitBool.cpp
#include <iostream>

class Pointer
{
private:
    int* ptr;

public:
    Pointer(int* p = nullptr) : ptr(p) {}

    // explicit — безпечно!
    explicit operator bool() const
    {
        return ptr != nullptr;
    }
};

int main()
{
    Pointer p1(new int(10));
    Pointer p2(nullptr);

    // ✅ Використання в умовах працює
    if (p1)
    {
        std::cout << "p1 is not null\n";
    }

    // ❌ Арифметичні операції заборонені
    // int sum = p1 + p2;  // ПОМИЛКА КОМПІЛЯЦІЇ!

    // ❌ Порівняння з числами заборонені
    // if (p1 == 1) {}     // ПОМИЛКА КОМПІЛЯЦІЇ!

    return 0;
}
./ExplicitBool
$ ./ExplicitBool
p1 is not null
Execution finished with exit code 0.

Рядок 12: explicit operator bool() дозволяє використання в контекстах, де очікується bool (умови if, while, !, &&, ||), але забороняє неявну конвертацію в інших контекстах.

Рядок 24: if (p1) працює, бо в умові if очікується bool, і компілятор дозволяє explicit конвертацію.

Рядки 31, 34 (закоментовані): арифметичні операції та порівняння з числами тепер заборонені. Компілятор видає помилку.

Правило безпеки для operator bool():ЗАВЖДИ робіть operator bool() з explicit. Це стандартна практика у сучасному C++. Стандартна бібліотека (std::ifstream, std::unique_ptr, std::optional) використовує саме explicit operator bool().

Ключове слово delete: заборона небажаних операцій

C++11 додав ключове слово = delete, що дозволяє явно заборонити виклик функції, конструктора або оператора. Це корисно для запобігання небажаним конвертаціям або операціям.

Заборона конструкторів-конвертерів

DeleteConstructor.cpp
#include <iostream>

class MyString
{
private:
    char* data;

public:
    MyString(const char* str)
    {
        // Копіюємо рядок
        int length = 0;
        while (str[length] != '\0') ++length;
        data = new char[length + 1];
        for (int i = 0; i <= length; ++i)
        {
            data[i] = str[i];
        }
        std::cout << "MyString constructed from const char*\n";
    }

    // Забороняємо конструктор із char — захист від помилок
    MyString(char) = delete;

    ~MyString()
    {
        delete[] data;
    }
};

int main()
{
    MyString s1("Hello");  // ✅ Працює

    // ❌ Заборонено — захищає від помилки
    // MyString s2('x');   // ПОМИЛКА КОМПІЛЯЦІЇ!
    // MyString s3 = 'A';  // ПОМИЛКА КОМПІЛЯЦІЇ!

    return 0;
}
./DeleteConstructor
$ ./DeleteConstructor
MyString constructed from const char*
Execution finished with exit code 0.

Рядок 23: MyString(char) = delete; забороняє створення MyString із одного символу char. Без цього рядка MyString s('x') компілювався б: компілятор перетворював би char на const char* (адреса символу), що призвело б до невизначеної поведінки.

Рядки 36–37 (закоментовані): спроби створити MyString із char тепер викликають помилку компіляції: use of deleted function.

Заборона небажаних перевантажень операторів

= delete також використовується для заборони певних перевантажень операторів, щоб запобігти небезпечним або безглуздим операціям.

DeleteOperator.cpp
#include <iostream>

class SafeInt
{
private:
    int value;

public:
    SafeInt(int v) : value(v) {}

    int getValue() const { return value; }

    // Дозволяємо порівняння з іншим SafeInt
    bool operator==(const SafeInt& other) const
    {
        return value == other.value;
    }

    // Забороняємо порівняння з double — може призвести до помилок точності
    bool operator==(double) const = delete;

    // Забороняємо ділення — щоб уникнути цілочисельного ділення без контролю
    SafeInt operator/(const SafeInt&) const = delete;
};

int main()
{
    SafeInt a(10);
    SafeInt b(10);
    SafeInt c(5);

    // ✅ Порівняння між SafeInt працює
    if (a == b)
    {
        std::cout << "a == b\n";
    }

    // ❌ Порівняння з double заборонено
    // if (a == 10.0) {}  // ПОМИЛКА КОМПІЛЯЦІЇ!

    // ❌ Ділення заборонено
    // SafeInt result = a / c;  // ПОМИЛКА КОМПІЛЯЦІЇ!

    return 0;
}
./DeleteOperator
$ ./DeleteOperator
a == b
Execution finished with exit code 0.

Рядок 20: bool operator==(double) const = delete; забороняє порівняння SafeInt із double. Це захищає від помилок точності плаваючої коми: 10 == 10.0000001 може дати неочікуваний результат.

Рядок 23: SafeInt operator/(const SafeInt&) const = delete; забороняє ділення між SafeInt. Це може бути корисно, якщо ви хочете примусити програміста явно обробляти цілочисельне ділення або використовувати інший метод.

Рядки 41, 44 (закоментовані): спроби використати заборонені операції викликають помилку компіляції з ясним повідомленням.


Заборона копіювання та присвоювання

Одне з найпоширеніших застосувань = delete — заборона копіювання об'єктів. Це корисно для класів, що керують унікальними ресурсами (файли, з'єднання, mutex).

NoCopy.cpp
#include <iostream>

class FileHandle
{
private:
    int fileDescriptor;

public:
    FileHandle(const char* filename)
    {
        // Симуляція відкриття файлу
        fileDescriptor = 42;  // Фіктивний дескриптор
        std::cout << "File opened: " << filename << "\n";
    }

    ~FileHandle()
    {
        std::cout << "File closed\n";
    }

    // Забороняємо копіювання — файл не можна дублювати
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    void write(const char* data)
    {
        std::cout << "Writing: " << data << "\n";
    }
};

int main()
{
    FileHandle file("data.txt");
    file.write("Hello, World!");

    // ❌ Копіювання заборонено
    // FileHandle file2 = file;       // ПОМИЛКА КОМПІЛЯЦІЇ!
    // FileHandle file3(file);        // ПОМИЛКА КОМПІЛЯЦІЇ!

    // ❌ Присвоювання заборонено
    // FileHandle file4("other.txt");
    // file4 = file;                  // ПОМИЛКА КОМПІЛЯЦІЇ!

    return 0;
}
./NoCopy
$ ./NoCopy
File opened: data.txt
Writing: Hello, World!
File closed
Execution finished with exit code 0.

Рядки 22–23: забороняємо конструктор копіювання та оператор присвоювання. Це гарантує, що об'єкт FileHandle існує лише в одній копії — не можна випадково продублювати файловий дескриптор, що призвело б до подвійного закриття.

Рядки 38–39, 43 (закоментовані): спроби копіювання або присвоювання викликають чітку помилку компіляції.

Зв'язок із std::unique_ptr: стандартний клас std::unique_ptr використовує саме цей патерн — копіювання заборонено через = delete, дозволено лише переміщення (move). Це гарантує, що покажчик завжди унікальний.

Порівняння: до C++11 vs після C++11

До C++11 для заборони копіювання використовували приватні конструктор копіювання та оператор присвоювання без реалізації:

class FileHandle
{
private:
    // Оголошення без реалізації — помилка лінкування при спробі виклику
    FileHandle(const FileHandle&);
    FileHandle& operator=(const FileHandle&);

public:
    FileHandle(const char* filename) { /* ... */ }
};

Переваги = delete:

  • Помилка виявляється на етапі компіляції, а не лінкування
  • Чітке повідомлення про помилку: use of deleted function
  • Явне документування наміру: код самодокументується
  • Можна забороняти не лише конструктори, але й будь-які функції та оператори
Сучасний стандарт: завжди використовуйте = delete замість старого патерну з приватними методами. Це робить намір явним і покращує діагностику помилок.

Практичний приклад: клас Money із захищеними конвертаціями

Об'єднаємо все, що ми вивчили, у безпечний клас Money із контрольованими конвертаціями:

SafeMoney.cpp
#include <iostream>
#include <cmath>

class Money
{
private:
    int dollars;
    int cents;

    void normalize()
    {
        if (cents >= 100)
        {
            dollars += cents / 100;
            cents = cents % 100;
        }
        else if (cents < 0)
        {
            int dollarsBorrowed = (-cents / 100) + 1;
            dollars -= dollarsBorrowed;
            cents += dollarsBorrowed * 100;
        }
    }

public:
    // explicit конструктор — захист від випадкових конвертацій
    explicit Money(int d = 0, int c = 0) : dollars(d), cents(c)
    {
        normalize();
    }

    // Забороняємо конструктор із double — занадто небезпечно для грошей
    Money(double) = delete;

    int getDollars() const { return dollars; }
    int getCents() const { return cents; }

    // explicit конвертація у double — лише явний виклик
    explicit operator double() const
    {
        return dollars + cents / 100.0;
    }

    // Заборона неявної конвертації у int — може призвести до втрати центів
    operator int() const = delete;

    // Дозволяємо явну перевірку на "порожній гаманець"
    explicit operator bool() const
    {
        return dollars != 0 || cents != 0;
    }

    // Оператори порівняння
    bool operator==(const Money& other) const
    {
        return dollars == other.dollars && cents == other.cents;
    }

    bool operator<(const Money& other) const
    {
        if (dollars != other.dollars)
            return dollars < other.dollars;
        return cents < other.cents;
    }

    // Забороняємо порівняння з числами — може призвести до помилок
    bool operator==(int) const = delete;
    bool operator==(double) const = delete;

    friend std::ostream& operator<<(std::ostream& out, const Money& money);
};

std::ostream& operator<<(std::ostream& out, const Money& money)
{
    out << "$" << money.dollars << ".";
    if (money.cents < 10) out << "0";
    out << money.cents;
    return out;
}

int main()
{
    // ✅ Явне створення
    Money wallet(50, 75);
    std::cout << "Wallet: " << wallet << "\n";

    // ❌ Неявна конвертація заборонена
    // Money m1 = 100;           // ПОМИЛКА: explicit конструктор
    // Money m2 = 19.99;         // ПОМИЛКА: конструктор із double видалений

    // ✅ Явне створення дозволено
    Money m3(100);
    Money m4 = Money(25, 50);
    std::cout << "m3: " << m3 << ", m4: " << m4 << "\n";

    // ✅ Використання у булевому контексті
    if (wallet)
    {
        std::cout << "Wallet is not empty\n";
    }

    Money empty;
    if (!empty)
    {
        std::cout << "Empty wallet evaluates to false\n";
    }

    // ✅ Явна конвертація у double
    double value = static_cast<double>(wallet);
    std::cout << "Wallet value: $" << value << "\n";

    // ❌ Заборонені операції
    // int i = wallet;           // ПОМИЛКА: operator int() видалений
    // double d = wallet;        // ПОМИЛКА: explicit operator double()
    // if (wallet == 50) {}      // ПОМИЛКА: operator==(int) видалений

    // ✅ Порівняння між Money працює
    if (m3 == Money(100))
    {
        std::cout << "m3 equals $100.00\n";
    }

    return 0;
}
./SafeMoney
$ ./SafeMoney
Wallet: $50.75
m3: $100.00, m4: $25.50
Wallet is not empty
Empty wallet evaluates to false
Wallet value: $50.75
m3 equals $100.00
Execution finished with exit code 0.

Рядок 27: explicit Money(int d = 0, int c = 0) — конструктор із explicit забороняє неявні конвертації. Money m = 100; не працює.

Рядок 33: Money(double) = delete; — захист від створення грошей із double (проблема точності: 19.99 може бути 19.989999...).

Рядок 39: explicit operator double() — конвертація у double лише через static_cast.

Рядок 45: operator int() const = delete; — заборона конвертації у int, щоб не втратити центи.

Рядок 48: explicit operator bool() — дозволяє перевірку if (wallet), але забороняє арифметичні операції.

Рядки 68–69: заборона порівняння з числами — захист від помилок на зразок if (wallet == 50) (програміст міг мати на увазі центи або долари?).

Рядки 91–92 (закоментовані): всі небезпечні операції заборонені і викликають чіткі помилки компіляції.

Принцип безпеки: клас Money демонструє defensive programming — захист від помилок через обмеження можливих операцій. Користувач класу не може випадково створити некоректний стан або виконати небезпечну операцію. Компілятор гарантує коректність на етапі компіляції.

Коли використовувати кожен механізм?

explicit конструктор

Використовуйте завжди, якщо конвертація не є очевидною та безпечною.

Приклади:

  • explicit Dollars(int amount) — захист від Dollars d = 100;
  • explicit FileHandle(const char* path) — файл не можна створити випадково

Виключення: типи-обгортки (std::string, std::vector), де неявна конвертація природна.

explicit operator bool()

Використовуйте ЗАВЖДИ для operator bool().

Причина: захист від небезпечних арифметичних операцій (obj + 1) та порівняння з числами (obj == 1).

Дозволяє: використання в умовах if (obj), while (obj), !obj.

= delete для конструкторів

Використовуйте для заборони небезпечних конвертацій.

Приклади:

  • Money(double) = delete; — заборона грошей із double (проблема точності)
  • MyString(char) = delete; — заборона створення рядка з одного символу

= delete для копіювання

Використовуйте для класів із унікальними ресурсами.

Приклади:

  • FileHandle(const FileHandle&) = delete; — файл не можна дублювати
  • Mutex(const Mutex&) = delete; — mutex не можна копіювати

Альтернатива: реалізувати глибоке копіювання (стаття 66).

explicit operator Type()

Використовуйте для захисту від неявних конвертацій.

Приклади:

  • explicit operator double() — конвертація лише через static_cast
  • explicit operator std::string() — явна конвертація у рядок

Без explicit: дозволяйте лише для природних конвертацій (Fractiondouble).

= delete для операторів

Використовуйте для заборони безглуздих або небезпечних операцій.

Приклади:

  • bool operator==(double) = delete; — заборона порівняння з double
  • SafeInt operator/(const SafeInt&) = delete; — заборона ділення

Резюме: безпека через обмеження

У цій статті ми вивчили механізми контролю конвертацій — потужні інструменти для створення безпечних класів:

  1. Конструктори як оператори конвертації — будь-який конструктор з одним параметром дозволяє неявну конвертацію
  2. explicit конструктор — забороняє неявну конвертацію, дозволяє лише явний виклик
  3. Оператори приведення типівoperator Type() дозволяє конвертувати об'єкт у інший тип
  4. explicit operator Type() — захист від неявної конвертації (особливо для bool)
  5. = delete — явна заборона функцій, конструкторів, операторів
Філософія сучасного C++: явність краща за неявність (explicit is better than implicit). Якщо операція може призвести до помилки, краще заборонити її на рівні компілятора, ніж покладатися на обережність програміста. Компілятор — ваш найкращий друг у виявленні помилок.

Анонс наступної статті

Ми завершили вивчення перевантаження операторів — від базових арифметичних до складних конвертацій. Але залишилася одна критична тема, без якої наше розуміння класів неповне: управління пам'яттю.

У наступній статті (стаття 66) ми повернемося до проблеми, що виникла у статті 59: поверхневе копіювання (shallow copy) класів із динамічною пам'яттю. Розглянемо:

  • Глибоке копіювання (deep copy) — як правильно копіювати об'єкти з динамічними ресурсами
  • Перевантаження operator= — оператор присвоювання із захистом від самоприсвоювання
  • Правило трьох (Rule of Three) — фундаментальне правило C++: деструктор + конструктор копіювання + оператор присвоювання
  • Практична реалізація — клас IntArray із повним управлінням пам'яттю

Це завершальна стаття серії про базові механізми ООП. Після неї ви зможете створювати безпечні, надійні класи, що коректно керують будь-якими ресурсами.

Copyright © 2026