Для чего нужен explicit конструктор в C++?

Ответ

Ключевое слово explicit применяется к конструкторам с одним параметром (или с несколькими, где все параметры, кроме первого, имеют значения по умолчанию) для запрета неявных преобразований типов. Это предотвращает неожиданное и потенциально ошибочное создание объектов компилятором.

Проблема без explicit:

class StringWrapper {
public:
    StringWrapper(const char* str) { /* ... */ } // Неявное преобразование разрешено
};

void processString(const StringWrapper& sw) { /* ... */ }

int main() {
    processString("hello"); // Компилятор неявно создаст временный объект StringWrapper.
    // Это может быть неочевидно и привести к трудноуловимым ошибкам.
}

Решение с explicit:

class StringWrapper {
public:
    explicit StringWrapper(const char* str) { /* ... */ } // Явный конструктор
};

void processString(const StringWrapper& sw) { /* ... */ }

int main() {
    // processString("hello"); // Ошибка компиляции: нельзя преобразовать 'const char*' в 'StringWrapper'
    processString(StringWrapper("hello")); // OK: явное создание объекта
    StringWrapper sw = "world"; // Ошибка: copy-initialization не сработает
    StringWrapper sw2("world"); // OK: direct-initialization
}

Когда использовать: Для классов, где неявное преобразование из типа параметра в тип класса не имеет смысла или опасно (например, классы-обертки, "умные" указатели, контейнеры). Конструкторы std::vector с одним параметром size_t являются explicit, чтобы избежать случайного создания вектора из целого числа.

Ответ 18+ 🔞

Давай разжую эту тему, а то некоторые думают, что explicit — это просто какая-то прихоть стандарта. На самом деле, это спасательный круг от ебушки-воробушки, которую может устроить компилятор, если ему дать волю.

Представь себе такую пиздопроебибну ситуацию. Написал ты класс-обёртку для строки, вроде бы всё просто.

class StringWrapper {
public:
    StringWrapper(const char* str) { /* ... */ } // Всё вроде ок, конструктор есть
};

А потом пишешь функцию, которая с этой обёрткой работает:

void processString(const StringWrapper& sw) { /* ... */ }

И тут, ёпта, начинается магия, от которой сам от себя охуеешь. Ты вызываешь функцию, передавая ей обычную сишную строку:

int main() {
    processString("hello"); // И оно РАБОТАЕТ! Но как, блядь?
}

А работает оно потому, что компилятор, этот хитрая жопа, видит: "Ага, функция хочет StringWrapper, а я получил const char*. Но у StringWrapper есть конструктор, который принимает const char*! Значит, я могу неявно, тихо, как маньяк, создать временный объект StringWrapper("hello") и передать его в функцию".

И вот ты сидишь, дебажишь какого-то хуя непонятную ошибку, а проблема в том, что вызов функции processString("hello") на самом деле вызывает не ту перегрузку, которую ты ожидал, или создаёт временный объект, который живёт не там и не столько, сколько нужно. Доверия ебать ноль к такой неявной возне.

Вот именно для этого и нужен explicit. Это такой крик разработчика компилятору: "Э, сабака сука! Не выёбывайся со своими неявными преобразованиями! Если хочешь сделать объект моего класса — делай это явно, на моих глазах!".

Исправленный вариант выглядит так:

class StringWrapper {
public:
    explicit StringWrapper(const char* str) { /* ... */ } // Ключевое слово explicit тут
};

void processString(const StringWrapper& sw) { /* ... */ }

int main() {
    // processString("hello"); // А теперь НЕ РАБОТАЕТ! Ошибка компиляции.
    // Компилятор говорит: "Чувак, я тебе больше не друг, конструктор explicit, иди лесом".

    processString(StringWrapper("hello")); // OK: вот теперь всё честно. Создал явно.

    // StringWrapper sw = "world"; // Тоже ошибка! Copy-initialization не прокатит.
    StringWrapper sw2("world"); // А вот так — direct-initialization — можно. Всё под контролем.
}

Когда это воткнуть, чтобы не было мучительно больно? Да почти всегда, когда конструктор принимает один аргумент (или когда у него несколько, но все после первого — со значениями по умолчанию), и этот аргумент — НЕ очевидная, прямая и единственная сущность для создания твоего объекта.

Классические примеры:

  • Классы-обёртки (как наш StringWrapper). const char* — это не строка-обёртка, это сырой указатель. Преобразование должно быть явным.
  • "Умные" указатели. Представь, если бы std::unique_ptr можно было неявно создать из сырого указателя... Будет вам хиросима и нигерсраки из утечек памяти и double-free.
  • Контейнеры. Взять тот же std::vector. У него есть конструктор, принимающий size_t — размер. Он explicit. И слава богу! А то написал бы ты void foo(const std::vector<int>& v); и вызвал foo(10), ожидая вектор с одним элементом 10. А компилятор бы тебе неявно создал вектор из 10 нулей. Волнение ебать было бы потом при отладке.

Короче, правило простое: подозрение ебать чувствую к конструктору с одним параметром? Ставь explicit. Лучше потом явно вызвать, чем часами искать, почему программа работает как хуй с горы. Экономит овердохуища нервных клеток.