Ответ
Ключевое слово 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. Лучше потом явно вызвать, чем часами искать, почему программа работает как хуй с горы. Экономит овердохуища нервных клеток.