четверг, 1 августа 2013 г.

Использование перегрузки операторов для enum в C++

На форуме stackoverflow.com можно найти массу советов по тому как вывести имя элемента enum в поток, но практически все они весьма сложные и громоздкие. Однако эту задачу можно решить гораздо проще используя средства самого языка.
Описанный ниже прием я узнал из книги Б. Страуструпа "Дизайн и эволюция С++" (проведя до этого не один час в раздумьях).
Не многие знают, что для типа enum в С++ есть возможность перегружать операторы. Особенно полезно это свойство когда вы генерируете некоторое выражение, например, на SQL.
В данной статье я покажу как можно использовать эту возможность языка С++ на примере вывода имени элемента перечисления в поток.


#include <iostream>
#include <string>
#include <stdexcept>

enum SqlOp
{
	Equal, 
	NotEqual,
	Less,
	Greater,
	LessEqual,
	GreaterEqual
};

inline std::istream& operator >> (std::istream& in, SqlOp& op)
{
	std::string s;
	in >> s;
	if (s == "=") {
		op = Equal;
	} 
	else if (s == "<>") {
		op = NotEqual;
	} 
	else if (s == "<") {
		op = Less;
	}
	else if (s == ">") {
		op = Greater;
	}
	else if (s == "<=") {
		op = LessEqual;
	}
	else if (s == ">=") {
		op = GreaterEqual;
	} else {
		throw std::runtime_error("Incorrect input");
	}
	return in;
}

inline std::ostream& operator << (std::ostream& out, const SqlOp& op)
{
	switch(op)
	{
	case Equal: 
		out << "=";
		break;
	case NotEqual:
		out << "<>";
		break;
	case Less:
		out << "<";
		break;
	case Greater:
		out << ">";
		break;
	case LessEqual:
		out << "<=";
		break;
	case GreaterEqual:
		out << ">=";
		break;
	}
	return out;
}


int main(int argc, char * argv[])
{
	using namespace std;
	SqlOp op = Equal;
	while(cin) {
		try {
			cin >> op;
		} catch(std::exception& e) {
			cout << "error: " << e.what() << endl;
			return 1;
		}
		cout << op << ": " << static_cast<int>(op) << endl;
	}
	return 0;
}


Перед Вами работающий код, который отображает концепцию: мы используем перегрузку операторов ввода и вывода в поток, чтобы создавать элементы перечисления из строки в потоке и записывать строку в соответствии с элементом в поток. Все прекрасно, однако, можем ли мы как-то улучшить наше решение? Если поразмыслить, то в приведенном выше отрывке мы ограничены классами потоков библиотеки STL. Чтобы снять это ограничение необходимо всего лишь сделать перегрузку операторов шаблонной:

enum SqlOp
{
	Equal, 
	NotEqual,
	Less,
	Greater,
	LessEqual,
	GreaterEqual
};

template<typename _IStream>
inline _IStream& operator >> (_IStream& in, SqlOp& op)
{
	std::string s;
	in >> s;
	if (s == "=") {
		op = Equal;
	} 
	else if (s == "<>") {
		op = NotEqual;
	} 
	else if (s == "<") {
		op = Less;
	}
	else if (s == ">") {
		op = Greater;
	}
	else if (s == "<=") {
		op = LessEqual;
	}
	else if (s == ">=") {
		op = GreaterEqual;
	} else {
		throw std::runtime_error("Incorrect input");
	}
	return in;
}

template<typename _OStream>
inline _OStream& operator << (_OStream& out, const SqlOp& op)
{
	switch(op)
	{
	case Equal: 
		out << "=";
		break;
	case NotEqual:
		out << "<>";
		break;
	case Less:
		out << "<";
		break;
	case Greater:
		out << ">";
		break;
	case LessEqual:
		out << "<=";
		break;
	case GreaterEqual:
		out << ">=";
		break;
	}
	return out;
}

Теперь мы можем использовать, например, потоки из библиотеки Qt для ввода/вывода нашего перечисления. Но и это еще не все: мы также можем использовать перегрузку операторов +, -, * и всех остальных операторов языка C++ разрешенных к перегрузке в пользовательских типах!

суббота, 13 апреля 2013 г.

Ключевое слово auto в C++: палка о двух концах?


В недавно вышедшем стандарте C++11 появилось новое ключевое слово auto, точнее сказать оно поменяло семантику. Это ключевое слово было в языке и раньше и обозначало автоматические (т.е. локальные) переменные. В новом же стандарте это ключевое слово предназначено для автоматического выведения типа переменной. Многие профессиональные программисты только рады нововведению, однако, как это часто бывает, - это палка о двух концах.
Давайте разберемся почему и зачем в языке C++ было добавлено это нововведение?
Для начала рассмотрим такой код:

#include <map>
#include <vector>
#include <string>
 
int main()
{
   using namespace std;
   map<string, vector< pair<int,double> > > parser;
   // ...
   map<string, vector< pair<int,double> > >::const_iterator it =
   parser.begin();
   for (; it != parser.end(); ++it) {
       // ...
   }
   return 0;
}
 
 

Видите нам пришлось написать весь этот громоздкий код (особенно в строке 10), только потому, что C++ язык строго типизированный и мы обязаны в объявлении переменной указать ее точный тип. Обычно такие проблемы решались через ключевое слово typedef, которое создает псевдоним типа:

#include <map>
#include <vector>
#include <string>
 
int main()
{
   using namespace std;
   typedef pair<int, double> pair_t;
   typedef vector<pair_t> vector_t;
   typedef map<string, vector_t> map_t;
   map_t parser;
   // ...
   map_t::const_iterator it = parser.begin();
   for (; it != parser.end(); ++it) {
      // ...
   }
   return 0;
}
 

Теперь нам будет проще разобраться в таком коде, однако все равно нам пришлось проделать много работы.

Переход на новый стандарт должен освободить программиста от всех этих порой ненужных объявлений псевдонимов типов. Однако, как уже говорилось выше, это палка о двух концах: для опытного программиста - это удобно и экономит время, однако даже такие люди порой могут только сделать хуже используя повсеместно ключевое слово auto, что уж и говорить о новичках...
В подтверждение типичный пример:

#include <vector>
 
int main()
{
   auto vctr = new std::vector<int>();
   vctr->push_back(3);
   int i = vctr[0]; // will not compiled
   //...
   delete vctr;
   return 0;
}
 
Что не так в этом коде? Да много чего:
  • Объявление auto равносильно std::vector<int>*, то есть объявляет тип "указатель на объект vector<int>".
  • Далее, vctr[0] равносильно *(vctr + 0), то есть *vctr, то есть разыменованному указателю на std::vector<int>.
  • Поскольку мы пытаемся присвоить результат разыменования указателя переменной типа int, то возникает ошибка компиляции, поскольку такое преобразование недопустимо в пределах этой единицы трансляции.
Как видите ошибиться нетрудно. Кроме этих недостатков есть еще один, который лично меня очень часто останавливает от использования этой новой возможности: до сих пор не все компиляторы поддерживают в полном объеме новый стандарт, и когда будут еще вопрос. Так что я бы сказал так: будьте внимательны и используйте auto с осторожностью и только по необходимости!

Запрет наследования в С++

Во многих современных языках программирования есть возможность объявить класс наследование от которого запрещено. В C# такие классы помечаются ключевым словом sealed, в Java - final. При попытке скомпилировать код с наследованием от таких классов приведет к ошибке компилятора.
А возможно ли такое же в C++?
Ответ: ДА!
Каким же образом создать такой класс на C++? Сразу хочу предупредить, что игры с конструкторами и деструкторами ни к чему не приведут... Сам я потратил не один час пытаясь такой класс написать)
Но все оказалось очень просто: достаточно скрыть в private части деструктор! Вот пример такого класса на C++:

class Sealed
{
private:
   ~Sealed() {}
};

Теперь при попытке написать класс наследник и скомпилировать код вы получите ошибку, гласящую, что невозможно создать класс наследник от класса содержащего закрытый деструктор! Вот так легко и без введения дополнительных ключевых слов удается реализовать поставленную задачу!

Об этом блоге и мне

Доброго времени суток!
Представляю Вашему вниманию мой личный блог по разнообразным аспектам  программирования в С++. Этот блог ставит своей задачей познакомить читателей с наработанными мною приемами программирования, а также некоторыми философскими размышлениями на тему)