Как известно, в С++, как и в его предке С, инструкция typdef не создает новых типов, а всего лишь псевдонимы для существующих. И если в слаботипизированном С это не было проблемой, то в строгом С++ это приводит к недоумению, особенно среди новичков, а так же трудноуловимым багам в программе.
В этой статье мы попробуем обойти эту неприятную особенность.
Рассмотрим пример
typedef unsigned int galosh_count_t;
typedef unsigned int cow_count_t;
void print (galosh_count_t count)
{
std::count << "У меня есть " << count << " пар галош!" << std::endl;
}
void print (cow_count_t count)
{
std::count << "У меня есть " << count << " коров!" << std::endl;
}
void print (galosh_count_t galosh_count, cow_count_t cow_count)
{
std::count << "У меня есть " << galosh_count << " пар галош и " << cow_count << " коров!" << std::endl;
}
int main (int, char*[])
{
galosh_count_t galosh_count = 10;
cow_count_t cow_count = 15;
print (cow_count, galosh_count); // Компилятор это проглотит, несмотря на то, что я перепутал порядок аргументов, а хотелось бы увидеть ошибку
print (galosh_count); // Ошибка, неоднозначный выбор перегруженной функции
print (cow_count); // Ошибка, неоднозначный выбор перегруженной функции
}
В этом примере мы имеем две ошибки компиляции и одну логическую ошибку. И если с невозможностью перегрузить функцию print с одним аргументом можно жить, то неверный порядок аргументов может привести к совершенно фантастическим и трудноуловимым багам в программе. Ревизией кода такие ошибки тоже выявить крайне сложно и чем больше аргументов в функции, тем сложнее.
Учим компилятор отличать коров от галош
Итак, как заставить typedef создавать новый тип вместо псевдонима? Конечно же при помощи шаблонов.
Попытка №1.
template <class T> class strong_type
{
public:
explicit strong_type (const T& val) : _val (val) {}
strong_type& operator = (const T& val) {_val = val; return *this;}
private:
T _val;
};
typedef strong_type<unsigned int> galosh_count_t;
typedef strong_type<unsigned int> cow_count_t;
Не сработает, galosh_count_t и cow_count_t все равно одного типа, т.к. компилятор инстанциирует класс strong_type только один раз с параметром unsigned int.
Чтобы заставить компилятор создать новый тип, мы добавим еще один аргумент в наш шаблон.
Попытка №1.
template <class T, class Tag> class strong_type
{
public:
explicit strong_type (const T& val) : _val (val) {}
strong_type& operator = (const T& val) {_val = val; return *this;}
private:
T _val;
};
typedef strong_type<unsigned int, class TAG_galosh_count_t> galosh_count_t;
typedef strong_type<unsigned int, class TAG_cow_count_t> cow_count_t;
Таким обрам мы имеем два разных типа galosh_count_t и cow_count_t. Теперь компилятор меня грязно обругает, если я вдруг перепутаю порядок аргументов и напротив не будет жаловаться на неоднозначность перегруженных функций.
Обратие внимание, что нам не нужно определять классы TAG_galosh_count_t и TAG_cow_count_t, они используются просто в качестве уникальных тэгов.
Впрочем, чтобы наш пример окончательно заработал, нам придется перегрузить оператор <<.
template <class T, class Tag> class strong_type
{
public:
explicit strong_type (const T& val) : _val (val) {}
strong_type& operator = (const T& val) {_val = val; return *this;}
template <class Stream>
Stream& operator << (Stream& s) const
{
s << _val;
return s;
}
private:
T _val;
};
typedef strong_type<unsigned int, class TAG_galosh_count_t> galosh_count_t;
typedef strong_type<unsigned int, class TAG_cow_count_t> cow_count_t;
В реальной программе конечно-же придется прегружать еще и арифметические, и логические операторы, чтобы коров и калоши можно было прибавлять, вычитать и сравнивать. Думаю сами справитесь.
Автор: Gunnar