пятница, 4 сентября 2009 г.

C#, Java или С++?... (продолжение)

Минуло несколько лет с тех пор как была написана первая статья по этой тематике. Как ни странно мое мнение ничуть не изменилось, а даже напротив укрепись. Как и раньше я считаю, что С++ является на сегодняшний день, хотя и сложным , но наиболее удобным, эффективным и продвинутым средством. Некоторые могут меня упрекнуть, что Java и C# более переносимы, на них проще писать, у них меньше проблем с наследованием и управлением памятью. Со всеми этими доводами я не согласен! 
Начнем попорядку.
С++ vs Java
Да, на Java писать программы проще, однако язык С++ по словам Б. Страуструпа с самого начала не предназначался для штампованных программистов пишущих штампованные программы. По поводу управления памятью могу привести цитату Б. Страуструпа из книги "Дизайн и эволюция С++ ":  
При проектировании С++ я сознательно решил не полагаться на автоматическую сборку мусора, опасаясь весьма значительных издержек по времени и памяти, которые удалось наблюдать в различных сборщиках мусора. <...> Общий вывод таков: сборка мусора желательна и возможна, но мы не можем позволить, чтобы от нее была зависима семантика С++ и большинства стандартных базовых библиотек.
 Данные утверждения хорошо иллюстрируются тем фактом, что в свое время шли разговоры (и даже были некоторые экспериментальные разработки) по созданию Java-процессоров напрямую поддерживающих инструкции Java. Вот насколько были плохи дела...
В Java реализованы шаблоны, но они сильно уступают по возможностям и гибкости шаблонам С++. Отсюда выдод: зачем нужны инструменты в новом языке, повторяющие уже созданные, но менее совершенные... не понятно...
Некоторые из читателей могу сказать, что Java абсолютно переносима. Нет скажу я Вам... Попробуйте перекомпилировать без виртуальной Java-машины код под процессоры SPARK, MIPS, что не получается? Правильно нужна Java-машина.. Что нет Java-машины под этот процессор? Давайте саму Java-машину перекомпилируем. А тут оп-па! программный код на С++! И кто тут переносимей?)))

С++ vs C#
С C# немного лучше в плане языка - тут тебе и управление памятью на низком уровне (Unmanaged code)  и перегрузку операторов для шаблонных можно реализовать (изрядно помявшись правда и спустившись до MASM'a, но кого это пугает!). Но вот незадача, даже под Linux ваша программа не пойдет - .NET Framework нужен! (стоит добавить что существует проект Mono для портирования программ на C# под Linux но он очень далек от совершенства). Заглянем внуть .NET Framework! И... О боже и здесь C++!

В заключении хочу сказать - многие, очень многие пророчут скорую смерть С++.
Бред! 
С++ будет еще очень долго существовать, поскольку реальной замены еще даже на горизонте не видно!

вторник, 25 августа 2009 г.

Создание класса сериализующегося с использованием различных потоков



При программировании на С++ иногда приходится сталкиваться с необходимостью реализовать в классе сериализацию в c использованием различных потоков, а не только с иcпользованием iostream. Для начала приведу пример сериализации ставший классическим:
#include <iostream>
#include <string>

class User
{
public:
User() : m_id(-1), m_balance(0)
{
}
User(long id,
std::string& name,
std::string& surname,
double balance) :
m_id(id),
m_name(name),
m_surname(surname),
m_balance(balance)
{
}

void setName(std::string& name) { m_name = name; }
std::string name() const { return m_name; }
void setSurname(std::string& surname) { m_surname = surname; }
std::string surname() const { return m_surname; }
void setBalance(double balance) { m_balance = balance; }
long id() const { return m_id; }

friend std::ostream& operator << (std::ostream& out, const User& user)
{
return out << m_id
<< m_name
<< m_surname
<< m_balance;
}
friend std::istream& operator >> (std::istream& in, const User& user)
{
in >> m_id
>> m_name
>> m_surname
>> m_balance;
return in;
}
private:
long m_id;
std::string m_name;
std::string m_surname;
double m_balance;
};



В приведенном коде используется так называемая перегрузка дружественных операторов, в данном случае операторов << и >>. Записать такой класс в поток (также как и считать обратно из потока) не сложно:



#include "User.h"
#include <iostream>
#include <fstream>

int main()
{
User u_(121001, "User1", "UserSurname", 123);
std::ofstream fout;
if (fout.open("test.dat", std::ios::out|std::ios::binary))
fout << u_;
fout.close();


std::ifstream fin;
User _u;
if (fout.open("test.dat", std::ios::out|std::ios::binary))
fin << _u;
fin.close();

cout << _u << endl;

}



Во многих случаях этого вполне достаточно, однако у такого подхода есть несколько минусов:
- Если есть иерархия классов, то дружественные операторы придется перегружать для каждого класса иерархии
- Сложно использовать другие средства сериализации (например, может потребоваться сериализация в xml), или точнее для каждого класса иерархии уже придется дописывать еще по два перегруженных оператора (при этом 100% где-то что-то забудешь).

Существует элегантный способ сделать сериализацию еще более гибкой, так что предлагаю на суд читателей свое изобретение:


//Файл iserializable.hpp
#ifndef ISERIALIZABLE_HPP
#define ISERIALIZABLE_HPP

#ifndef interface
#define interface struct
#endif

template <typename _Stream>
interface ISerializable
{
virtual void pack(_Stream& out) const = 0;
virtual void unpack(_Stream& in) = 0;

};

#endif


//Файл User.h

#ifndef USER_H
#define USER_H


#include <iostream>

// Следующие два заголовка
// из библиотеки Qt
#include <QTextStream>
#include <QDataStream>


class User :
public ISerializable<std::iostream>,
public ISerializable<QTextStream>,
public ISerializable<QDataStream>
{
public:
User() : m_id(-1), m_balance(0){}
User(long id,
std::string& name,
std::string& surname,
double balance) :
m_id(id),
m_name(name),
m_surname(surname),
m_balance(balance)

{
}

virtual void setName(std::string& name) { m_name = name; }
virtual std::string name() const { return m_name; }
virtual void setSurname(std::string& surname) { m_surname = surname; }
virtual std::string surname() const { return m_surname; }
virtual void setBalance(double balance) { m_balance = balance; }

long id() const { return m_id; }

virtual void pack(std::iostream& out) const
{
out << m_id << m_name << m_surname << m_balance;
}

virtual void unpack(std::iostream& in)
{
in >> m_id >> m_name >> m_surname >> m_balance;
}

virtual void pack(QTextStream& out) const
{
out << m_id << m_name << m_surname << m_balance;
}
virtual void unpack(QTextStream& in)
{
in >> m_id >> m_name >> m_surname >> m_balance;
}

virtual void pack(QDataStream& out) const
{
out << m_id << m_name << m_surname << m_balance;
}
virtual void unpack(QDataStream& in)
{
in >> m_id >> m_name >> m_surname >> m_balance;
}


private:

long m_id;
std::string m_name;
std::string m_surname;
double m_balance;

};



class AdvancedUser : public User

{

public:

AdvancedUser() : User()
{
}

AdvancedUser( long id,
std::string& name,
std::string& surname,
double balance,
std::string& advanced) :
User(id, name, surname, balance),
m_advancedData(advanced)

{
}

void setAdvancedData(std::string& dat) { m_advancedData = dat; }
std::string advancedData() const { return m_advancedData; }

virtual void pack(iostream& out) const
{
User::unpack(in);
out << advancedData;
}



virtual void unpack(iostream& in)
{
User::pack(out);
in >> m_advancedData;
}



virtual void pack(QTextStream& out) const
{
User::pack(out);
out << advancedData;
}



virtual void unpack(QTextStream& in)
{
User::unpack(in);
in >> m_advancedData;
}



virtual void pack(QDataStream& out) const
{
User::pack(out);
out << advancedData;
}


virtual void unpack(QDataStream& in)
{
User::unpack(in);
in >> m_advancedData;
}


private:

std::string m_advancedData;
};


#endif




Идея предлагаемого подхода состоит в следующем: создается абстрактный шаблонный интерфейс, который должен быть инстанциирован по потоку. Далее наследуем наш класс от этого интерфейса и реализуем функции чтения и записи. Казалось бы все просто? Ан, нет... При наследовании от интерфейса мы обязаны явно указать параметры шаблона. Магия в том, что интерфейс написан один раз, и наследуемся мы вроде бы тоже от одного класса, а в реальности получается множественное наследование! В некотором смысле в приведенном примере используется статический полиморфизм, однако мы обязаны реализовать теперь не 2 функции а 6 (по 2 на каждый инстанциированный шаблон). Некоторые могут возразить что приведенный пример не скомпилируется из-за того что имена функций чтения/записи одинаковы, но это не совсем так: просто получается, что мы делаем эти функции перегруженными.
Другие читатели могут заметить, что возникнут проблемы если два раза пронаследоваться от интерфейса проинстанциированным по одному и тому же типу потока. Сразу хочу заметить: такое не скомпилируется (ошибка времени компиляции: уже здорово, гораздо лучше чем ошибка времени выполнения), и кроме того: часто ли вы по ошибке наследуетесь от одного и того же класса? )))
А вот самые внимательные читатели обнаружат, что начал я с прегрузки операторов вводо-вывода в поток, что же с ними? Тут очередной (хотя и вполне очевидный) трюк: перегрузить операторы ввода-вывода потребуется только один раз! Действительно, поскольку класс User в нашем примере полиморфный, то операторы ввода-вывода можно сделать глобальными и описать следующим образом:


std::iostream& operator << (const User& usr, std::iostream& out)
{ usr.pack(out); }

std::iostream& operator >> (const User& usr, std::iostream& in)
{ usr.unpack(in); }



QTextStream& operator << (const User& usr, QTextStream& out)
{ usr.pack(out); }

QTextStream& operator >> (const User& usr, QTextStream& in)
{ usr.unpack(in); }



QDataStream& operator << (const User& usr, QDataStream& out)
{ usr.pack(out); }

QDataStream& operator >> (const User& usr, QDataStream& in)
{ usr.unpack(in); }


понедельник, 24 августа 2009 г.

Блокирование наследования в С++

Представим на минуту что мы пишем инструментальную библиотеку, в которой есть менеджер сессий входа в систему, причем реализовать его требуется как синглтон и кроме того заблокировать возможность наследования от этого класса, поскольку пр наследовании и переопределении методов менеджера программа может работать неустойчиво или попросту падать.
Итак перед нами интересный вопрос на эрудицию: как заблокировать наследование от класса в С++? Напомню, что в C# и Java такая возможность существует. Как же ее реализовать в С++?
Многие скорее всего кинуться делать макросы или просто попытаются пихнуть в базовый класс какую-нибудь статическую переменную для проверки пронаследован класс или нет. Однако это не поможет. При та5ком подходе мы упускаем одну очень важную деталь: все эти проверки будут происходить во времяы выполнения и кроме того всегда возвращать неверные значения, поскольку по правилам C++ конструктор базового класса ВСЕГДА вызывается первым.
Так как же быть?
Ответ настолько же прост насколько и сложен: необходимо перенести деструктор класса в private часть класса. Читатель может возразить: при чем же здесь деструктор?

Предлагаю взглянуть на следующий фрагмент кода:


class SessionManager
{
public:
bool login(std::string& name, std::string& password);
void logout();
static bool create();
static bool destroy();
static SessionManager* instance();
private:
static SessionManager* m_self;
// блокируем констрктор
SessionManager();
// блокируем деструктор!!!
~SessionManager();
// блокируем оператор присваивания
SessionManager& operator = (const SessionManager&);
};

class SessionManagerDerived : public SessionManager
{
............
............
static bool create() {
// ошибка времени компиляции:
// невозможно создать объект класса с закрытым деструктором!
if (!m_self) return new SessionManagerDerived();
return m_self;
}
};



Вот и вся фишка: компилятор не позволит создать класс с закрытым деструктором, поскольку не знает как его удалять! Замечу что для класса SessionManager компилятор имеет эти данные хоть они и в private части.