Приведення типів, explicit та delete
Приведення типів, explicit та delete
Невидимі конвертації: коли компілятор занадто «розумний»
У попередніх статтях ми навчили наші класи працювати з операторами: додавання, множення, порівняння. Код виглядає природно і читабельно. Але C++ має ще одну потужну — і водночас небезпечну — можливість: автоматичне приведення типів (implicit type conversion).
Розглянемо просту ситуацію:
#include <iostream>
class Seconds
{
private:
int totalSeconds;
public:
Seconds(int initialSeconds = 0) : totalSeconds(initialSeconds) {}
int getTotalSeconds() const
{
return totalSeconds;
}
void print() const
{
std::cout << totalSeconds << " s";
}
};
void setTimer(Seconds duration)
{
std::cout << "Timer set for: ";
duration.print();
std::cout << "\n";
}
int main()
{
Seconds taskDuration(50);
setTimer(taskDuration); // ✅ Очевидно: передаємо об'єкт Seconds
// ❓ А це?
setTimer(100); // ⚠️ Компілюється! Чому?
setTimer(3.14); // ⚠️ Працює з втратою даних! double → int → Seconds
return 0;
}
Що відбулося? На рядку 32 компілятор бачить виклик setTimer(100) і аналізує типи: функція очікує аргумент типу Seconds, але отримує ціле число int. Компілятор знаходить конструктор Seconds(int initialSeconds) і автоматично створює тимчасовий об'єкт Seconds(100). Проте розробник міг мати на увазі 100 мілісекунд! У результаті таймер очікуватиме понад півтори хвилини замість однієї десятої секунди. Це поширене джерело помилок у мережевих сокетах та багатопотокових сервісах.
Рядок 33: у виклику setTimer(3.14) спочатку число з рухомою комою типу double автоматично усікається до цілого числа int зі значенням 3 (стандартне приведення), після чого створюється тимчасовий об'єкт Seconds(3) (користувацьке приведення). Дробова частина безповоротно втрачається без жодного попередження компілятора.
Такі неявні конвертації можуть призвести до тонких помилок, коли компілятор «здогадується» неправильно, і код компілюється без попереджень, але працює не так, як очікувалося.
У цій статті ми навчимося контролювати ці конвертації: дозволяти лише явні, забороняти небезпечні, створювати власні оператори приведення типів.
- Implicit conversion (неявна конвертація) — компілятор автоматично перетворює один тип на інший
- Explicit conversion (явна конвертація) — програміст явно вказує приведення типу через
static_castабо викликає конструктор - User-defined conversion (користувацька конвертація) — конвертація, визначена програмістом через конструктори або оператори приведення
Конструктори як оператори конвертації
Будь-який конструктор, що приймає один аргумент, компілятор розглядає як оператор неявної конвертації. Це означає, що коли компілятор бачить невідповідність типів, він автоматично викликає такий конструктор.
#include <iostream>
class Fraction
{
private:
int numerator;
int denominator;
public:
// Конструктор із одним параметром — оператор конвертації int → Fraction
Fraction(int numeratorValue) : numerator(numeratorValue), denominator(1)
{
std::cout << "Converting " << numeratorValue << " to Fraction\n";
}
Fraction(int numeratorValue, int denominatorValue)
: numerator(numeratorValue), denominator(denominatorValue) {}
void print() const
{
std::cout << numerator << "/" << denominator;
}
};
void printFraction(Fraction fraction)
{
fraction.print();
std::cout << "\n";
}
int main()
{
Fraction firstFraction(3, 4);
printFraction(firstFraction); // ✅ Прямий виклик
// Неявна конвертація int → Fraction
printFraction(5); // ⚠️ Створюється Fraction(5, 1)
// Неявна конвертація у виразах
Fraction secondFraction = 7; // ⚠️ Еквівалентно Fraction secondFraction(7);
return 0;
}
Рядок 11: конструктор Fraction(int numeratorValue) приймає один параметр. Компілятор дозволяє використовувати його для автоматичної конвертації int → Fraction.
Рядок 36: printFraction(5) — компілятор бачить невідповідність: функція очікує Fraction, але отримує int. Компілятор викликає Fraction(5), створює тимчасовий об'єкт Fraction(5, 1), передає його у функцію.
Рядок 39: Fraction secondFraction = 7 — не викликається operator=, бо об'єкт ще не існує. Це ініціалізація, що еквівалентна Fraction secondFraction(7). Компілятор викликає конвертуючий конструктор.
void processAccount(BankAccount account). Якщо у класі BankAccount є конструктор BankAccount(int accountNumber), то виклик processAccount(12345) компілюється без помилок. Але розробник міг мати на увазі щось інше (наприклад, передати суму балансу), і код працює некоректно. Компілятор не попередить про помилку.Ключове слово explicit: заборона неявних конвертацій
Щоб заборонити компілятору автоматично викликати конструктор для конвертації, використовуємо ключове слово explicit. Воно дозволяє лише явні виклики конструктора.
#include <iostream>
class Fraction
{
private:
int numerator;
int denominator;
public:
// explicit забороняє неявну конвертацію int → Fraction
explicit Fraction(int numeratorValue) : numerator(numeratorValue), denominator(1)
{
std::cout << "Explicit constructor called\n";
}
Fraction(int numeratorValue, int denominatorValue)
: numerator(numeratorValue), denominator(denominatorValue) {}
void print() const
{
std::cout << numerator << "/" << denominator;
}
};
void printFraction(Fraction fraction)
{
fraction.print();
std::cout << "\n";
}
int main()
{
Fraction firstFraction(3, 4);
printFraction(firstFraction); // ✅ Працює
// ❌ Неявна конвертація заборонена
// printFraction(5); // ПОМИЛКА КОМПІЛЯЦІЇ!
// ✅ Явна конвертація дозволена
printFraction(Fraction(5)); // Явний виклик конструктора
printFraction(static_cast<Fraction>(7)); // Явне приведення
// ❌ Неявна ініціалізація заборонена
// Fraction secondFraction = 9; // ПОМИЛКА КОМПІЛЯЦІЇ!
// ✅ Явна ініціалізація дозволена
Fraction thirdFraction(9); // Пряма ініціалізація
Fraction fourthFraction{11}; // Uniform initialization
return 0;
}
Рядок 11: ключове слово explicit перед конструктором забороняє неявні конвертації. Компілятор не дозволить автоматичний виклик цього конструктора.
Рядок 36 (закоментований): printFraction(5) тепер помилка компіляції. Компілятор не може автоматично створити Fraction із цілого числа int.
Рядок 39–40: явний виклик конструктора Fraction(5) або явне приведення static_cast<Fraction>(7) дозволені. Розробник явно вказує свій намір.
Рядок 43 (закоментований): Fraction secondFraction = 9 — помилка компіляції. Синтаксис із = інтерпретується як неявна конвертація.
Рядки 46–47: пряма ініціалізація Fraction thirdFraction(9) або спискова ініціалізація Fraction fourthFraction{11} працюють, бо це явні виклики конструктора.
explicit, якщо неявна конвертація не є очевидною і бажаною. Це захищає від випадкових помилок. Виключення: типи-обгортки на зразок std::string, де std::string s = "hello"; є природним.Оператори приведення типів: user-defined conversions
Окрім конвертації в наш клас (через конструктори), C++ дозволяє визначити конвертацію з нашого класу в інші типи. Це робиться через оператори приведення типів (conversion operators).
Синтаксис:
operator ТипПризначення() const
{
// Конвертуємо поточний об'єкт у ТипПризначення
return значення;
}
Особливості:
- Немає типу повернення (тип вказано в імені оператора)
- Зазвичай
const-метод (не змінює об'єкт) - Компілятор викликає автоматично при невідповідності типів
#include <iostream>
class Fraction
{
private:
int numerator;
int denominator;
public:
Fraction(int numeratorValue, int denominatorValue)
: numerator(numeratorValue), denominator(denominatorValue)
{
}
// Оператор конвертації Fraction → double
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 fraction(7, 2); // 7/2 = 3.5
std::cout << "Fraction: ";
fraction.print();
std::cout << "\n\n";
// Неявна конвертація Fraction → double
printDouble(fraction); // Викликається operator double()
// Явна конвертація через static_cast
double doubleValue = static_cast<double>(fraction);
std::cout << "\nExplicit conversion:\n";
std::cout << "Double: " << doubleValue << "\n";
// Неявна конвертація у виразах
double calculationResult = fraction + 1.5; // fraction → double, потім 3.5 + 1.5
std::cout << "\nfraction + 1.5 = " << calculationResult << "\n";
return 0;
}
Рядки 16–19: operator double() перетворює дріб на double, обчислюючи ділення numerator / denominator. Компілятор викликає цей оператор автоматично, коли очікується double, але передається Fraction.
Рядок 42: printDouble(fraction) — компілятор бачить невідповідність типів: функція очікує double, але отримує Fraction. Компілятор знаходить operator double() і викликає його.
Рядок 51: вираз fraction + 1.5 — компілятор перетворює fraction на double (через operator double()), потім виконує 3.5 + 1.5 = 5.0.
operator int(), вирази на зразок fraction + 1.5 або fraction + 1 стали б неоднозначними. Компілятор не знав би, чи конвертувати fraction у double чи в int для відповідного вбудованого оператора +. Це викличе помилку компіляції: ambiguous overload. Щоб уникнути цього, використовуйте оператори приведення обережно.explicit для операторів приведення
Так само, як для конструкторів, ключове слово explicit можна застосовувати до операторів приведення типів. Це забороняє неявну конвертацію, дозволяючи лише явну.
#include <iostream>
class Fraction
{
private:
int numerator;
int denominator;
public:
Fraction(int numeratorValue, int denominatorValue)
: numerator(numeratorValue), denominator(denominatorValue)
{
}
// 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 fraction(3, 4);
// ❌ Неявна конвертація заборонена
// printDouble(fraction); // ПОМИЛКА КОМПІЛЯЦІЇ!
// double doubleValue = fraction; // ПОМИЛКА КОМПІЛЯЦІЇ!
// double calculationResult = fraction + 1.5; // ПОМИЛКА КОМПІЛЯЦІЇ!
// ✅ Явна конвертація дозволена
printDouble(static_cast<double>(fraction));
double doubleValue = static_cast<double>(fraction);
std::cout << "Explicit conversion: " << doubleValue << "\n";
return 0;
}
Рядки 17–20: explicit operator double() дозволяє конвертацію лише через явний static_cast або пряме приведення.
Рядки 39–41 (закоментовані): усі неявні конвертації тепер заборонені. Компілятор видасть помилку: cannot convert 'Fraction' to 'double' without a explicit conversion.
Рядки 44–45: явне приведення static_cast<double>(fraction) працює. Програміст явно вказує намір конвертувати.
Спеціальний випадок: explicit operator bool()
Один із найпоширеніших патернів — конвертація об'єкта в bool для використання в умовах (if, while). Наприклад, std::ifstream можна перевіряти: if (file) { ... }.
Але operator bool() без explicit може призвести до дуже дивних помилок:
#include <iostream>
class SmartPointer
{
private:
int* managedPointer;
public:
explicit SmartPointer(int* initialPointer = nullptr) : managedPointer(initialPointer) {}
~SmartPointer()
{
delete managedPointer;
}
// НЕ explicit — небезпечно!
operator bool() const
{
return managedPointer != nullptr;
}
};
int main()
{
SmartPointer firstPointer(new int(10));
SmartPointer secondPointer(nullptr);
// ✅ Передбачувана поведінка
if (firstPointer)
{
std::cout << "firstPointer is not null\n";
}
// ❌ Несподівана поведінка: компілюється, але помилка!
int sumResult = firstPointer + secondPointer; // firstPointer → bool → int (1), secondPointer → bool → int (0), sumResult = 1
std::cout << "sumResult = " << sumResult << "\n"; // sumResult = 1
// ❌ Ще гірше: можна порівнювати з числами!
if (firstPointer == 1) // firstPointer → bool → int, порівнюємо int == int
{
std::cout << "firstPointer == 1 ?!\n";
}
return 0;
}
Рядок 36: firstPointer + secondPointer — компілятор перетворює firstPointer і secondPointer на bool (через operator bool()), потім bool на int (стандартна конвертація true → 1, false → 0), потім додає їх. Результат: 1 + 0 = 1. Об'єкти покажчиків додаються як звичайні числа — це логічна помилка, але код компілюється без зауважень!
Рядок 40: firstPointer == 1 — компілятор перетворює firstPointer на bool, потім на int, порівнює 1 == 1. Працює, але семантично неправильно.
Рішення: використовуйте explicit operator bool():
#include <iostream>
class SmartPointer
{
private:
int* managedPointer;
public:
explicit SmartPointer(int* initialPointer = nullptr) : managedPointer(initialPointer) {}
~SmartPointer()
{
delete managedPointer;
}
// explicit — безпечно!
explicit operator bool() const
{
return managedPointer != nullptr;
}
};
int main()
{
SmartPointer firstPointer(new int(10));
SmartPointer secondPointer(nullptr);
// ✅ Використання в умовах працює
if (firstPointer)
{
std::cout << "firstPointer is not null\n";
}
// ❌ Арифметичні операції заборонені
// int sumResult = firstPointer + secondPointer; // ПОМИЛКА КОМПІЛЯЦІЇ!
// ❌ Порівняння з числами заборонені
// if (firstPointer == 1) {} // ПОМИЛКА КОМПІЛЯЦІЇ!
return 0;
}
Рядки 17–21: explicit operator bool() дозволяє використання у контекстах, де явно очікується логічне значення bool (умови if, while, оператори !, &&, ||), але забороняє неявне приведення в арифметичних виразах.
Рядок 30: if (firstPointer) працює, бо в умові if очікується bool, і компілятор дозволяє explicit конвертацію.
Рядки 36, 39 (закоментовані): арифметичні операції та порівняння з числами тепер заборонені. Компілятор видає помилку.
operator bool():ЗАВЖДИ робіть operator bool() з explicit. Це стандартна практика у сучасному C++. Стандартна бібліотека (std::ifstream, std::unique_ptr, std::optional) використовує саме explicit operator bool().Ключове слово delete: заборона небажаних операцій
C++11 додав ключове слово = delete, що дозволяє явно заборонити виклик функції, конструктора або оператора. Це корисно для запобігання небажаним конвертаціям або операціям.
Заборона конструкторів-конвертерів
#include <iostream>
class MyString
{
private:
char* textBuffer;
public:
MyString(const char* initialString)
{
// Копіюємо рядок
int stringLength = 0;
while (initialString[stringLength] != '\0') ++stringLength;
textBuffer = new char[stringLength + 1];
for (int characterIndex = 0; characterIndex <= stringLength; ++characterIndex)
{
textBuffer[characterIndex] = initialString[characterIndex];
}
std::cout << "MyString constructed from const char*\n";
}
// Забороняємо конструктор із char — захист від помилок
MyString(char singleCharacter) = delete;
~MyString()
{
delete[] textBuffer;
}
};
int main()
{
MyString firstString("Hello"); // ✅ Працює
// ❌ Заборонено — захищає від помилки
// MyString secondString('x'); // ПОМИЛКА КОМПІЛЯЦІЇ!
// MyString thirdString = 'A'; // ПОМИЛКА КОМПІЛЯЦІЇ!
return 0;
}
Рядок 23: MyString(char singleCharacter) = delete; забороняє створення MyString із одного символу char. Без цього рядка спроба виклику могла б призвести до небажаних приведень типу.
Рядки 38–39 (закоментовані): спроби створити MyString із char тепер викликають помилку компіляції: use of deleted function.
Заборона небажаних перевантажень операторів
= delete також використовується для заборони певних перевантажень операторів, щоб запобігти небезпечним або безглуздим операціям.
#include <iostream>
class SafeInt
{
private:
int storedValue;
public:
explicit SafeInt(int initialValue) : storedValue(initialValue) {}
int getValue() const { return storedValue; }
// Дозволяємо порівняння з іншим SafeInt
bool operator==(const SafeInt& otherNumber) const
{
return storedValue == otherNumber.storedValue;
}
// Забороняємо порівняння з double — може призвести до помилок точності
bool operator==(double floatingValue) const = delete;
// Забороняємо ділення — щоб уникнути цілочисельного ділення без контролю
SafeInt operator/(const SafeInt& divisorNumber) const = delete;
};
int main()
{
SafeInt firstNumber(10);
SafeInt secondNumber(10);
SafeInt thirdNumber(5);
// ✅ Порівняння між SafeInt працює
if (firstNumber == secondNumber)
{
std::cout << "firstNumber == secondNumber\n";
}
// ❌ Порівняння з double заборонено
// if (firstNumber == 10.0) {} // ПОМИЛКА КОМПІЛЯЦІЇ!
// ❌ Ділення заборонено
// SafeInt divisionResult = firstNumber / thirdNumber; // ПОМИЛКА КОМПІЛЯЦІЇ!
return 0;
}
Рядок 19: bool operator==(double floatingValue) const = delete; забороняє порівняння SafeInt із double. Це захищає від помилок точності плаваючої коми: 10 == 10.0000001 може дати неочікуваний результат.
Рядок 22: SafeInt operator/(const SafeInt& divisorNumber) const = delete; забороняє ділення між SafeInt. Це спонукає програміста явно обирати спосіб обчислення або використовувати інший метод.
Рядки 39, 42 (закоментовані): спроби використати заборонені операції викликають помилку компіляції з ясним повідомленням.
Заборона копіювання та присвоювання
Одне з найпоширеніших застосувань = delete — заборона копіювання об'єктів. Це корисно для класів, що керують унікальними ресурсами (файли, з'єднання, mutex).
#include <iostream>
class FileHandle
{
private:
int fileDescriptor;
public:
FileHandle(const char* targetFilename)
{
// Симуляція відкриття файлу
fileDescriptor = 42; // Фіктивний дескриптор
std::cout << "File opened: " << targetFilename << "\n";
}
~FileHandle()
{
std::cout << "File closed\n";
}
// Забороняємо копіювання — файл не можна дублювати
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
void write(const char* dataToWrite)
{
std::cout << "Writing: " << dataToWrite << "\n";
}
};
int main()
{
FileHandle activeFile("data.txt");
activeFile.write("Hello, World!");
// ❌ Копіювання заборонено
// FileHandle duplicateFile = activeFile; // ПОМИЛКА КОМПІЛЯЦІЇ!
// FileHandle anotherFile(activeFile); // ПОМИЛКА КОМПІЛЯЦІЇ!
// ❌ Присвоювання заборонено
// FileHandle secondaryFile("other.txt");
// secondaryFile = activeFile; // ПОМИЛКА КОМПІЛЯЦІЇ!
return 0;
}
Рядки 23–24: забороняємо конструктор копіювання та оператор присвоювання. Це гарантує, що об'єкт FileHandle існує лише в одному екземплярі — не можна випадково продублювати файловий дескриптор, що призвело б до подвійного закриття.
Рядки 39–40, 44 (закоментовані): спроби копіювання або присвоювання викликають чітку помилку компіляції.
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* targetFilename) { /* ... */ }
};
class FileHandle
{
public:
FileHandle(const char* targetFilename) { /* ... */ }
// Явна заборона — помилка компіляції із зрозумілим повідомленням
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
Переваги = delete:
- Помилка виявляється на етапі компіляції, а не лінкування
- Чітке повідомлення про помилку: use of deleted function
- Явне документування наміру: код самодокументується
- Можна забороняти не лише конструктори, але й будь-які функції та оператори
= delete замість старого патерну з приватними методами. Це робить намір явним і покращує діагностику помилок.Практичний приклад: клас MemorySize із захищеними конвертаціями
Об'єднаємо все, що ми вивчили, у безпечний клас MemorySize із контрольованими конвертаціями:
#include <iostream>
#include <cstdint>
class MemorySize
{
private:
uint64_t bytesCount;
public:
// explicit конструктор — захист від випадкових конвертацій
explicit MemorySize(uint64_t initialBytes) : bytesCount(initialBytes) {}
// Конструктор за замовчуванням та для цілочисельних літералів int
explicit MemorySize(int initialBytes = 0)
: bytesCount(initialBytes >= 0 ? static_cast<uint64_t>(initialBytes) : 0)
{
}
// Забороняємо конструктор із double — байти не можуть бути дробовими
MemorySize(double) = delete;
uint64_t getBytes() const
{
return bytesCount;
}
// explicit конвертація у double (мегабайти) — лише явний виклик
explicit operator double() const
{
return static_cast<double>(bytesCount) / (1024.0 * 1024.0);
}
// Заборона неявної конвертації у int — захист від переповнення та втрати даних
operator int() const = delete;
// Дозволяємо явну перевірку на ненульовий розмір
explicit operator bool() const
{
return bytesCount > 0;
}
// Оператори порівняння між об'єктами MemorySize
bool operator==(const MemorySize& otherSize) const
{
return bytesCount == otherSize.bytesCount;
}
bool operator<(const MemorySize& otherSize) const
{
return bytesCount < otherSize.bytesCount;
}
// Забороняємо порівняння з числами — захист від плутанини одиниць вимірювання
bool operator==(int) const = delete;
bool operator==(double) const = delete;
friend std::ostream& operator<<(std::ostream& outputStream, const MemorySize& memorySize);
};
std::ostream& operator<<(std::ostream& outputStream, const MemorySize& memorySize)
{
outputStream << memorySize.bytesCount << " B";
return outputStream;
}
int main()
{
// ✅ Явне створення
MemorySize allocatedBuffer(52428800); // 50 МБ у байтах
std::cout << "Allocated buffer: " << allocatedBuffer << "\n";
// ❌ Неявна конвертація заборонена
// MemorySize firstBuffer = 1048576; // ПОМИЛКА: explicit конструктор
// MemorySize secondBuffer = 12.5; // ПОМИЛКА: конструктор із double видалений
// ✅ Явне створення дозволено
MemorySize firstBuffer(1048576); // 1 МБ
MemorySize secondBuffer = MemorySize(2097152); // 2 МБ
std::cout << "firstBuffer: " << firstBuffer << ", secondBuffer: " << secondBuffer << "\n";
// ✅ Використання у булевому контексті
if (allocatedBuffer)
{
std::cout << "Allocated buffer is not empty\n";
}
MemorySize emptyBuffer;
if (!emptyBuffer)
{
std::cout << "Empty buffer evaluates to false\n";
}
// ✅ Явна конвертація у double (мегабайти)
double megabytesValue = static_cast<double>(allocatedBuffer);
std::cout << "Allocated buffer in megabytes: " << megabytesValue << " MB\n";
// ❌ Заборонені операції
// int integerBytes = allocatedBuffer; // ПОМИЛКА: operator int() видалений
// double rawMegabytes = allocatedBuffer; // ПОМИЛКА: explicit operator double()
// if (allocatedBuffer == 52428800) {} // ПОМИЛКА: operator==(int) видалений
// ✅ Порівняння між MemorySize працює
if (firstBuffer == MemorySize(1048576))
{
std::cout << "firstBuffer equals 1048576 B (1 MB)\n";
}
return 0;
}
Рядки 10–16: explicit MemorySize(...) — конструктори із explicit забороняють неявні конвертації із числових типів. Запис MemorySize buffer = 1048576; не скомпілюється.
Рядок 19: MemorySize(double) = delete; — захист від створення об'єкта розміру пам'яті з чисел із рухомою комою (кількість байтів не може бути дробовою).
Рядки 27–30: explicit operator double() — конвертація у double (переведення байтів у мегабайти) дозволена виключно через явний static_cast.
Рядок 33: operator int() const = delete; — пряма неявна конвертація в int заборонена задля уникнення переповнення діапазону та випадкової втрати даних.
Рядки 36–39: explicit operator bool() — дозволяє перевірку if (allocatedBuffer), але надійно блокує випадкову участь об'єкта в арифметичних виразах.
Рядки 49–50: заборона порівняння з числами (operator==(int) та operator==(double)) — захист від семантичних помилок (компілятор не дозволить порівняти буфер із числом, оскільки неможливо вгадати, що мав на увазі розробник: байти, кілобайти чи мегабайти).
Рядки 95–97 (закоментовані): усі небезпечні операції заборонені і призводять до однозначних повідомлень про помилку компіляції.
MemorySize демонструє підхід захисного програмування (defensive programming) — унеможливлення помилок через суворе обмеження допустимих операцій. Користувач класу не може випадково створити некоректний стан чи виконати неоднозначне приведення типів. Компілятор гарантує коректність на етапі компіляції.Коли використовувати кожен механізм?
explicit конструктор
Використовуйте завжди, якщо конвертація не є очевидною та безпечною.
Приклади:
explicit Seconds(int initialSeconds)— захист відSeconds taskDuration = 100;explicit FileHandle(const char* targetFilename)— файл не можна створити випадково
Виключення: типи-обгортки (std::string, std::vector), де неявна конвертація природна.
explicit operator bool()
Використовуйте ЗАВЖДИ для operator bool().
Причина: захист від небезпечних арифметичних операцій (obj + 1) та порівняння з числами (obj == 1).
Дозволяє: використання в умовах if (obj), while (obj), !obj.
= delete для конструкторів
Використовуйте для заборони небезпечних конвертацій.
Приклади:
MemorySize(double) = delete;— заборона створення розміру пам'яті з дробових чиселMyString(char singleCharacter) = delete;— заборона створення рядка з одного символу
= delete для копіювання
Використовуйте для класів із унікальними ресурсами.
Приклади:
FileHandle(const FileHandle&) = delete;— файл не можна дублюватиMutex(const Mutex&) = delete;— mutex не можна копіювати
Альтернатива: реалізувати глибоке копіювання (стаття 66).
explicit operator Type()
Використовуйте для захисту від неявних конвертацій.
Приклади:
explicit operator double()— конвертація лише черезstatic_castexplicit operator std::string()— явна конвертація у рядок
Без explicit: дозволяйте лише для природних конвертацій (Fraction → double).
= delete для операторів
Використовуйте для заборони безглуздих або небезпечних операцій.
Приклади:
bool operator==(double) = delete;— заборона порівняння зdoubleSafeInt operator/(const SafeInt&) = delete;— заборона ділення
Резюме: безпека через обмеження
У цій статті ми вивчили механізми контролю конвертацій — потужні інструменти для створення безпечних класів:
- Конструктори як оператори конвертації — будь-який конструктор з одним параметром дозволяє неявну конвертацію
explicitконструктор — забороняє неявну конвертацію, дозволяє лише явний виклик- Оператори приведення типів —
operator Type()дозволяє конвертувати об'єкт у інший тип explicit operator Type()— захист від неявної конвертації (особливо дляbool)= delete— явна заборона функцій, конструкторів, операторів
Анонс наступної статті
Ми завершили вивчення перевантаження операторів — від базових арифметичних до складних конвертацій. Але залишилася одна критична тема, без якої наше розуміння класів неповне: управління пам'яттю.
У наступній статті (стаття 66) ми повернемося до проблеми, що виникла у статті 59: поверхневе копіювання (shallow copy) класів із динамічною пам'яттю. Розглянемо:
- Глибоке копіювання (deep copy) — як правильно копіювати об'єкти з динамічними ресурсами
- Перевантаження
operator=— оператор присвоювання із захистом від самоприсвоювання - Правило трьох (Rule of Three) — фундаментальне правило C++: деструктор + конструктор копіювання + оператор присвоювання
- Практична реалізація — клас
IntArrayіз повним управлінням пам'яттю
Це завершальна стаття серії про базові механізми ООП. Після неї ви зможете створювати безпечні, надійні класи, що коректно керують будь-якими ресурсами.
Перевантаження операторів: методи класу та спеціальні оператори
Унарні оператори (-, !, ~), інкремент та декремент (++, --), оператор індексації [], оператор виклику (), оператори присвоювання. Перевантаження через методи класу.
Глибоке копіювання та оператор присвоювання
Shallow vs deep copy, перевантаження operator=, самоприсвоювання, Rule of Three. Правильне управління динамічною памяттю у класах.