Ключевым словом explicit на C++ предотвращаются случайные преобразования типов. Изучим его во всех нюансах: принцип работы, использование и важность в повседневном программировании на C++.

Чем на самом деле занимается explicit

Ключевым словом explicit блокируются неявные преобразования, выполняемые конструкторами или операторами преобразования. Вот простой пример:

class Number {
public:
Number(int x) : value(x) {} // Неявное преобразование разрешается
int value;
};

class ExplicitNumber {
public:
explicit ExplicitNumber(int x) : value(x) {} // Неявное преобразование блокируется
int value;
};

void process_number(Number n) {
std::cout << n.value << "\n";
}

void process_explicit_number(ExplicitNumber n) {
std::cout << n.value << "\n";
}

int main() {
process_number(42); // Выполняется: неявное преобразование из «int» в «Number»
process_explicit_number(42); // Ошибка: неявное преобразование не разрешается
process_explicit_number(ExplicitNumber(42)); // Выполняется: явное преобразование
}

Реальный пример: смарт-обработчик ресурсов

Вот практический пример, которым показывается важность explicit в управлении ресурсами:

class FileHandler {
public:
explicit FileHandler(const std::string& filename)
: file_(filename.c_str(), std::ios::out) {
if (!file_.is_open()) {
throw std::runtime_error("Failed to open file");
}
}

void write(const std::string& data) {
file_ << data;
}

private:
std::ofstream file_;
};

// Использование
void save_data(FileHandler& handler, const std::string& data) {
handler.write(data);
}

int main() {
FileHandler handler("data.txt"); // Корректно
save_data(handler, "Hello");

// Не скомпилируется, так предотвращается случайное создание временного файла
save_data("temp.txt", "Hello"); // Ошибка: нет неявного преобразования
}

explicit и малозаметные баги

Вот классический пример того, как при помощи explicit предотвращаются баги в числовых преобразованиях:

class Percentage {
public:
explicit Percentage(double value) : value_(value) {
if (value < 0 || value > 100) {
throw std::out_of_range("Percentage must be between 0 and 100");
}
}

double get() const { return value_; }

private:
double value_;
};

void apply_discount(const Percentage& discount, double& price) {
price *= (1.0 - discount.get() / 100.0);
}

int main() {
double price = 100.0;

Percentage discount(10.0); // Корректно: скидка 10 %
apply_discount(discount, price);

// Не скомпилируется, так предотвращается случайное применение «сырых» чисел
apply_discount(50.0, price); // Ошибка: нет неявного преобразования

// Процент «percentage» необходимо создать явно
apply_discount(Percentage(50.0), price); // Корректно
}

explicit в классах template

Ключевое слово explicit особенно кстати приходится в классах template:

template<typename T>
class Container {
public:
explicit Container(size_t size) : data_(size) {}
explicit Container(const T& value) : data_(1, value) {}

size_t size() const { return data_.size(); }

private:
std::vector<T> data_;
};

void process_container(const Container<int>& c) {
// Логика обработки
}

int main() {
Container<int> c1(5); // Создается контейнер с размером «5»
Container<int> c2(42); // Создается контейнер с одним элементом

// Не скомпилируются:
process_container(10); // Ошибка: нет неявного преобразования из «int»
Container<int> c3 = 5; // Ошибка: нет неявного преобразования
}

explicit с многопараметрическим конструктором

Начиная с C++20, explicit используется с конструкторами, которыми принимается несколько параметров:

class Point {
public:
explicit Point(double x = 0, double y = 0) : x_(x), y_(y) {}

double x() const { return x_; }
double y() const { return y_; }

private:
double x_, y_;
};

void draw_point(const Point& p) {
// Логика отрисовки
}

int main() {
Point p1(1.0, 2.0); // OK
Point p2 = {1.0, 2.0}; // Ошибка: явный конструктор
draw_point({1.0, 2.0}); // Ошибка: нет неявного преобразования
draw_point(Point(1.0, 2.0)); // OK: явный конструктор
}

explicit в смарт-указателях

explicit важен при работе со смарт-указателями:

template<typename T>
class UniquePtr {
public:
explicit UniquePtr(T* ptr = nullptr) : ptr_(ptr) {}

// Предотвращаются неявные преобразования между различными типами «UniquePtr»
template<typename U>
explicit UniquePtr(UniquePtr<U>&& other) {
ptr_ = other.release();
}

~UniquePtr() { delete ptr_; }

T* release() {
T* tmp = ptr_;
ptr_ = nullptr;
return tmp;
}

private:
T* ptr_;

// Предотвращается копирование
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
};

class Base {};
class Derived : public Base {};

void process_base(UniquePtr<Base> ptr) {
// Логика обработки
}

int main() {
Base* raw_ptr = new Base();
UniquePtr<Base> ptr1(raw_ptr); // OK

UniquePtr<Derived> derived_ptr(new Derived());
// Не скомпилируется, так предотвращается случайная передача владения
process_base(derived_ptr); // Ошибка: нет неявного преобразования

// Владение необходимо передать явно
process_base(UniquePtr<Base>(derived_ptr.release())); // OK
}

explicit в современных интерфейсах C++

Вот как при помощи explicit совершенствуется проектирование API в современном C++:

class NetworkConfig {
public:
explicit NetworkConfig(const std::string& host, uint16_t port)
: host_(host), port_(port) {}

explicit NetworkConfig(const std::string& connection_string) {
parse_connection_string(connection_string);
}

// явное преобразование в строку
explicit operator std::string() const {
return host_ + ":" + std::to_string(port_);
}

private:
std::string host_;
uint16_t port_;

void parse_connection_string(const std::string& conn_str) {
// Логика синтаксического анализа
}
};

class NetworkClient {
public:
explicit NetworkClient(const NetworkConfig& config) : config_(config) {}

private:
NetworkConfig config_;
};

int main() {
// Компилируются:
NetworkConfig config1("localhost", 8080);
NetworkConfig config2("localhost:8080");
NetworkClient client(config1);

// Не компилируются:
NetworkClient client2("localhost:8080"); // Ошибка: нет неявного преобразования
std::string conn_str = config1; // Ошибка: нет неявного преобразования
std::string explicit_str = std::string(config1); // OK: явное преобразование
}

Заключение

  1. Используйте explicit для однопараметральных конструкторов, если неявное преобразование лишено семантического смысла.
  2. Всегда используйте explicit для операторов преобразования.
  3. Используйте explicit для многопараметральных конструкторов на C++20.
  4. Придерживайтесь единообразного использования explicit в иерархиях классов.
  5. Документируйте, когда и почему вы предпочитаете не использовать explicit.

Помните: explicit призван делать поведение кода более понятным и предотвращать случайные преобразования. Используйте его, если неявные преобразования чреваты багами или нечётким кодом. Не используйте explicit, если с неявными преобразованиями код становится более удобным для восприятия и безопасным.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: Complete Guide to explicit in C++

Предыдущая статьяApple убивает Swift
Следующая статьяКак защитить сайт от скрейперов