Заставляем компилятор отличать коров от галош

в 9:37, , рубрики: c++, коровы, С++, метки: ,

Как известно, в С++, как и в его предке С, инструкция 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


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js