Как ключевое слово explicit меняет возможность неявного преобразования через конструктор?
explicit запрещает использовать конструктор как неявное преобразование типа. Такой конструктор всё ещё можно вызвать явно: при прямой инициализации, явном приведении или подходящей форме list-initialization.
Без explicit конструктор, который можно вызвать с одним аргументом, может участвовать в передаче аргументов, возврате значений и других контекстах неявного преобразования. Это удобно для действительно естественных преобразований, но опасно, если создание объекта связано с существенным смыслом или проверками.
C++ поддерживает пользовательские преобразования типов, чтобы классы могли вести себя естественно в выражениях. В частности, конструктор, доступный с одним аргументом, может быть преобразующим: язык автоматически создаёт объект целевого класса из аргумента исходного типа.
Такая возможность облегчает работу с небольшими типами-обёртками, но может скрывать создание объектов и допускать вызовы, которые выглядят случайными. explicit предоставляет автору класса способ оставить явное создание доступным, одновременно отключив неявное.
Предположим, функция принимает тип Money, а его конструктор принимает число. Если конструктор не помечен explicit, вызов с числом может неявно создать денежный объект. Компилятор сочтёт такой вызов корректным, хотя число может быть указано в неправильных единицах, иметь неподходящую точность или вообще быть передано по ошибке.
Неверное решение опасно тем, что ошибка проявляется не в месте конструирования объекта, а в месте вызова функции. При добавлении explicit такие преобразования становятся диагностируемыми: вызывающий код должен явно показать намерение создать объект.
Рассмотрим минимальный пример:
Без explicit последний конструктор мог бы автоматически преобразовать 42 в UserId при вызове load_user(42). С explicit конструктор не участвует в обычных неявных преобразованиях, но прямое создание UserId{42} остаётся разрешённым.
Важно различать прямую инициализацию и копирующую инициализацию. Вызов конструктора в форме UserId id(42) или UserId id{42} допускает explicit-конструктор, тогда как инициализация через присваивающую форму вроде UserId id = 42 — нет.
То же ограничение действует при передаче аргумента функции, возврате значения и выборе перегрузки, когда компилятору потребовалось бы неявно создать объект. Явное приведение, например через static_cast<UserId>(42), напротив, выражает намерение и может использовать explicit-конструктор.
Конструктор считается преобразующим не только при ровно одном явно объявленном параметре. Если у него несколько параметров, но остальные имеют значения по умолчанию, его также можно вызвать с одним аргументом; поэтому вопрос о explicit нужно рассматривать с учётом фактической формы вызова.
В C++20 допустима условная форма explicit, зависящая от compile-time-условия. Она полезна в шаблонных типах, когда возможность неявного преобразования должна зависеть от свойств типа, но базовый принцип остаётся тем же: явное создание разрешено, неявное — только при выполнении условия.
Обычно explicit предпочтителен для идентификаторов, денежных величин, единиц измерения, дескрипторов ресурсов и других типов, где преобразование не является очевидным и безопасным. Неявный конструктор оправдан, когда преобразование действительно ожидаемо, не теряет смысл и не создаёт неоднозначностей.
В библиотеке платежей тип Money создавался из double, а функция списания принимала Money. Благодаря неявному конструктору вызов с числом компилировался, но не показывал, в какой валюте и с какой точностью интерпретируется значение.
Рассматривались два варианта. Первый — оставить неявное преобразование: вызовы короче, но ошибки единиц измерения и случайная передача чисел остаются плохо заметными. Второй — сделать конструктор explicit и добавить фабрику вроде from_cents или from_rubles: код становится немного длиннее, зато место преобразования фиксирует смысл значения.
Выбран второй вариант. Он не запрещает создание Money, но переносит ответственность за интерпретацию числа в явно названный код. В результате ошибочные вызовы функций обнаруживаются компилятором, а ревьюеру проще проверить единицы измерения.
Запрещает ли explicit прямое создание объекта?
Нет. Его назначение — запретить неявное преобразование, а не сам вызов конструктора. Прямая инициализация скобками или фигурными скобками, явное приведение и некоторые формы явного вызова по-прежнему доступны.
Поэтому explicit не делает тип неконструируемым и не заменяет модификаторы доступа. Он меняет только контексты, в которых компилятор может самостоятельно выбрать этот конструктор для преобразования.
Всегда ли конструктор с одним параметром является преобразующим?
Не обязательно в практическом смысле: он может быть объявлен explicit, и тогда использовать его как неявное преобразование нельзя. Кроме того, преобразующим потенциально является конструктор с несколькими параметрами, если у всех параметров после первого есть значения по умолчанию.
Поэтому корректнее говорить о конструкторе, который можно вызвать с одним аргументом, а затем отдельно учитывать наличие explicit. Это важно при анализе перегрузок и шаблонного кода.
Чем explicit-конструктор отличается от explicit оператора преобразования?
explicit-конструктор управляет преобразованием из другого типа в тип класса. explicit оператор преобразования управляет преобразованием из объекта класса в другой тип.
Например, конструктор может запрещать автоматическое создание UserId из числа, а explicit-оператор может запрещать автоматическое получение числа из UserId. В обоих случаях явное приведение остаётся возможным, но случайное участие преобразования в перегрузке, условии или передаче аргумента предотвращается.