Matvey Cherevko

Об исключительной пользе explicit (16 Jun 13)

Так уж повелось, что конструкторы в C++ по-умолчанию implicit, то есть поддерживают неявное преобразование, что порождает груду случайных ошибок. Приведу пример из жизни. Жили-были два класса математических векторов:

class vec2f {
	float x;
	float y;
public:
	vec2f(float x, float y);
	vec2f(const vec2f& other);
	// ...
};
class vec3f {
	float x;
	float y;
	float z;
public:
	vec3f(float x, float y, float z);
	vec3f(const vec3f& other);
	// ...
};

И всё у них было хорошо, пока кто-то не решил добавить в них “удобств”, а именно, конструкторы из одного в другой:

vec2f::vec2f(const vec3f& other)
: x(other.x), y(other.y) {}

vec3f::vec3f(const vec2f& other)
: x(other.x), y(other.y), z(0.f) {}

Все обрадовались, как теперь всё стало хорошо, и начали этим добром пользоваться. В другом государстве, при этом, жила функция camera_scale, которая о двумерном мире ничего не знала, да и знать не хотела:

void camera_scale(const vec3f& scale);

И однажды весь мир этот исчез… Долго думали собравшись братцы, кто же и где же накосячил, пока в дебаге не обнаружили, что мир по Z-оси заскейлен в ноль. Догадливый читатель уже понял в чем дело:

vec2f v(1.f, 2.f);
camera_scale(v);

Вызывалась функция camera_scale с двумерным вектором (1;2), который неявно преобразовывался в вектор трехмерный (1;2;0) и мир весь при этом “сжимался” до нуля… Решение проблемы этой заключается в простом запрещении неявного преобразования, а именно в дописывании explicit нужным конструкторам:

explicit vec2f(const vec3f& other);
explicit vec3f(const vec2f& other);

Теперь на код, из сказки выше, компилятор будет ругаться нехорошими словами, а братцы, которые его писали, всегда объявляют конструкторы с одним параметром explicit, если явно не нужно обратного.

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

Кого не убедила сказка выше, могу дать еще один кусок кода с этой проблемой:

struct String {
	// преаллоцировать n знакомест
	String(size_t n) {
		::printf("string(size_t n)");
	}
	// конструктор из raw-строки
	String(const char* str) {
		::printf("string(const char* str)");
	}
};
void main() {
	String str = 'n';
}

Угадайте, что выведет программа? ;) Именно! string(size_t n). Решение простое - дописать explicit для конструктора с параметром size_t, из const char* запрещать неявное преобразование не нужно, потому что тогда бы наш чудо-класс строки стал бы менее удобен.