Оптимизированные грабли

Тут на днях коллеги подложили нам свинью.

#define true false    // happy debugging!

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

Система наша не то чтобы старая. Политкорректно выражаясь, можно сказать, что она имеет историю. И, как в приличных домах, состоит из ядра и множества внешних компонентов, поставляющих данные в систему. Компоненты эти общаются с ядром через свой собственный несложный API и могут быть написаны вообще кем угодно и на чем угодно. Что и случалось раньше и даже по сей день случается.

(more…)

Паттерн “observer” на tr1::function или почти настоящие делегаты

Про C++ делегаты на основе boost::function (tr1::function) я уже писал. В отличие от стандартного варианта обсервера для C++, который подразумевает фиксированный контракт (интерфейс) для подписчиков, вариант с функциональными объектами обладает определенными весомыми преимуществами (все они перечислены в посте по ссылке).

Но за удобство приходится платить. В данном случае расплачиваться приходится невозможностью реализовать отписку. И причина тому банально проста. Вот простейший пример

class Subject
{
   public:
       typedef std::tr1::function<void (const MyData&)> typeDelegate;

       void Subscribe(typeDelegate);
       void Unsubscribe(typeDelegate);
}

Реализация метода Subscribe() вопросов не вызывает. А вот с отпиской возникает проблема. В случае с классическим обсервером в оба метода передается голый указатель на интерфейс подписчика. А указатели можно сравнивать без хитростей, поэтому отписка превращается в банальное удаление элемента из контейнера.

Function сравнивать нельзя. В tr1 этот класс не определяет операторы сравнения, да и сама идея сравнивать функции в общем виде выглядит несколько нелепо. Идея подобного функционального объекта состоит в том, чтобы единожды его создав, скрыть его внутренности от всех остальных, при этом предоставив возможность создавать копии и выполнять как функцию с предопределенной сигнатурой. Эта идея не подразумевает возможности сравнивать, так как непонятно, что тут вообще можно сравнить. В бусте вроде пытались сделать оператор равенства для функций, но ничего хорошего из этого не вышло.

Отсутствие возможности сравнивать ставит жиирный такой знак вопроса на самой возможности отписки. Конечно, можно усложнить интерфейс субъектов и обязать подписчиков передавать какой-нибудь уникальный идентификатор при подписке и отписке. Таким идентификатором, например, может быть и указатель на подписчика, совсем как в классическом обсервере.

Делать так можно. Только это кривовато сразу по нескольким статьям. Поэтому нам нужно пойти другим путем.

Вышеозначенный делегат можно создавать динамически и заворачивать в умный указатель. А умные указатели уже можно сравнивать! Значит, получим

class Subject
{
   public:
       typedef std::tr1::function<void (const MyData&)> typeDelegate;
       typedef std::tr1::shared_ptr<typeDelegate> typeDelegatePtr;

       void Subscribe(typeDelegatePtr);
       void Unsubscribe(typeDelegatePtr);
}

Теперь, чтобы подписаться на события субъекта, подписчик должен сначала динамически создать объект типа typeDelegate, завернуть его в shared_ptr, а затем уже этот умный указатель использовать для подписки и отписки:

// Somewhere in subscriber's constructor, for example
Subject::typeDelegatePtr m_Delegate(
    new Subject::typeDelegate(bind(&Subscriber::Process, this, _1))
    );
//...
//   Subscribe for events on _pSubject
_pSubject->Subscribe(m_Delegate);
//
//
//   Unsubscribe from _pSubject
_pSubject->Unsubscribe(m_Delegate);

При этом реализация методов подписки и отписки субъекта становится очевидной, поэтому о них больше ни слова.

Как видно, если у объекта есть необходимость отписки, он должен сохранять оригинал делегата для этих целей. Если отписка не требуется (а такое бывает часто), локальная копия m_Delegate не нужна – временный экземпляр его можно будет создать при вызове Subscribe().

Можно еще сделать следующий финт ушами – слегка изменить интерфейсы и в субъект передавать не shared_ptr, а weak_ptr, образованный от оригинального умного указателя на делегат, который является членом класса-подписчика. При этом бесплатно решается проблема курицы и яйца: иногда требуется обеспечить, чтобы объект-подписчик автоматически отписывался при удалении, но никакой гарантии, что субъект еще жив, нет и быть не может. Достаточно лишь обеспечить, чтобы субъект проверял, не протухли ли “слабые” указатели (или делал им lock()) перед вызовом делегата, а подписчик владел оригинальным shared_ptr<typeDelegate>. При этом удаление подписчика будет вызывать инвалидацию всех инстанций weak_ptr, образованных от этого указателя, тем самым делая невозможным вызов убитого объекта через делегат, что и даст нам такую вожделенную возможность подписчика замести все свои следы даже без информации о том, на события каких объектов он подписан и живы ли они еще.

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

Fuck the standards!

TR1 уже давно вышел. Это хорошо. Студия 2008 и 2010 уже включают его по умолчанию. Это просто отлично.

Стандарт (а TR1 – это де-факто уже стандарт) – это всегда хорошо. Больше нельзя называться C++ программистом и закатывать истерики при виде безобидного bind. Теперь нужно либо напрячь мозги и таки освоить основы функциональщины, либо идти заниматься гуевой мышевозней.

А еще стандарты – это плохо. Потому что любое, даже самое разумное и оправданное изменение стандарта превращается в процесс перемещения и горы и Магомета к единому стандартизированному месту встречи. И стандартно занимает до чертиков времени, да и других восполнимых и не очень ресурсов тоже дофига требует.

Вот включили они bind в стандарт. И чо (что, шо, So what, нужное подчеркнуть)? А перегруженные операторы не включили! Бо не было их еще в бусте на момент подачи черновиков в стандарт. В итоге в TR1 вот так вот не сделать:

vector<Person> myVector;
//
// Some stuff here
//
vector<Person>::iterator iter = find_if(
myVector.begin(),
myVector.end(),
bind(&Person::GetLastName, _1) == "Pupkin");

А все из-за того, что тот bind, что в TR1, не перегружает оператор “==”, сцуко такой! В итоге, чтобы сделать то же самое, что на куске кода вверху, нужно городить двухуровневый bind с equal_to посередине, от вида которого даже у привычных к boost пассажиров может случится истерика:

vector<Person>::iterator iter = find_if(
myVector.begin(),
myVector.end(),
bind(equal_to<string>, "Pupkin", bind(&Person::GetLastName, _1)));

Еще можно по-старинке взять и нарисовать функтор. На каждый подобный чих. В итоге от обилия их изжога начнется уже у меня.

Нинавижу.

А следующего стандарта еще 10 лет ждать.

\Microsoft was unexpected at this time

I just spent two days tracking this problem down.

I received my new development machine with Windows7 x64 only a week ago. It all was just pefect – quad core CPU could compile the world in almost no time, and brand new Visual Studio 2010 was just as good as it gets. By the way, I am the Linux guy and prefer vim, automake and gcc any other IDE, so if I call Visual Studio perfect, it means something.

Neverthless, everything good has an end. Yesterday I tried to build bcp.exe tool I needed to dissect boost, and I faced very nasty and very unclear problem. I simply could not. Every time I started the build process, it would fail with the most weird message I ever seen:

\Microsoft was unexpected at this time

Well, I mentioned I am the Linux guy, but I am not a religious fanatic. I just thought that there was a problem of some sort in the boost’s build script and used another machine to build bcp.

Next day I became really concerned. I realized that I could not use “Visual Studio Prompt” from VS 2010 and VS 2008. Every time I tried to start them, they would have that message about unexpected microsoft on them; and environment variables were not set. So I could not build anything! Sadly, this was rather a major problem for me as we had quite a few makefile projects and not being able to set the correct environment for build process was that kind of present I could happily live without.

I spent next four hours to find that this or similar problem was already mentioned several times; but nobody offered a solution that would work and nobody explained what was causing that. Some said that Windows SDK installation could break the batch file vcvars32.bat; some thought there was a bug in the batch file itself. Strangely,  it all worked perfectly on the the neighbor’s machine. I even compared the files and found no difference at all.

The solution came at the end of the day when all my hopes were gone. One guy mentioned that the problem was caused by the parentheses in vcvars32.bat:

@if not "%WindowsSdkDir%" == "" (
	@set "PATH=%WindowsSdkDir%bin\NETFX 4.0 Tools;%WindowsSdkDir%bin;%PATH%"
	@set "INCLUDE=%WindowsSdkDir%include;%INCLUDE%"
	@set "LIB=%WindowsSdkDir%lib;%LIB%"
)

He mentioned that as he found out, one of the directories where SDK was had braces in the name as well, and that broke the statement above completely as batch processor would find nested parentheses in the folder’s name and consider them as the end of the statement.

Needless to mention, may machine had some paths that had braces in them. C:\Program Files (x86). And, needless to mention, it is exactly where Visual Studio is installed. And it also exists in %PATH% environment variable.

By itself, it does not break the script. As I mentioned, it worked perfectly on the other machine which was the biological twin of my PC. But, what if one of the PATH’s members is encolsed in double quotes? (to check how your PATH looks like, type echo %PATH% in command prompt). I checked and that’s what I found:

c:\winutils;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;
C:\Windows\System32\WindowsPowerShell\v1.0\;
c:\Program Files (x86)Microsoft SQL Server\100\Tools\Binn\;
c:\Program Files\Microsoft SQL Server\100\Tools\Binn\;
c:\Program Files\Microsoft SQL Server\100\DTS\Binn\;
"C:\Program Files (x86)Microsoft Visual Studio 9.0\Common7\IDE\";
C:\Program Files (x86)Microsoft Team Foundation Server 2010 Power Tools\;
C:\Program Files (x86)Microsoft Team Foundation Server 2010 Power Tools\Best Practices Analyzer\;
c:\cygwin\bin

Bingo! I found that one entry of PATH was indeed enclosed in double quotes. And, not surprisingly, it had that nasty (x86) in it. It was pointing somewhere inside VS 2008 installation tree, so I think one of VS 2008 tools I have installed recently indeed screwed this up.

So, I simply removed those double quotes. Immediately after that, both VS 2010 and VS 2008 command prompts were fixed.

So, the fist thing you should do if you’re having the same problem: look at your %PATH% environment variable and check if anything is enclosed in double quotes.

Finding this just cost me two days of work, and I hope it it helps you sorting it out much faster.

\Microsoft was unexpected at this time

A day at the work. Untold story about good, bad and boost.

Ух, планов у меня сегодня было громадье. И то сделать хотел, и это и еще вон то. Как всегда, сработал закон Мерфи – чем больше задумаешь, тем меньше сделаешь.

Ну, обо всем по порядку. Нужен нам в проекте умный указатель. Так нужен, что аж спать не можем. Только едим, да. Еще нужны bind и function до кучи. Что хорошо, контора наконец-то переросла подростковое желание переизобрести велосипед, поэтому вумный указатель писать сами в этот раз не стали, а решили взять готовый. Из TR1, буста у нас почему-то боятся как огня, несмотря на присутствие в конторе автора boost::asio.

Одна загвоздка – проект на VS 2005, для которого TR1 выпущен не был. Что плохо. А компилировать проект в итоге мы будем именно 2005 студией по множеству причин, которые к делу не относятся. Для нас это означало известного размера геморрой при портировании кода прототипа с 2010 Студии на 2005.  Знаю, это  все уже начинает походить на прямую трансляцию из театра абсурда. Кто-то из архитекторов даже предложил добавить в пьесу еще один акт. Сказал, что фигня вопрос, мы просто скопируем нужные хедеры из 2008 и дело в шляпе.

В общем, с позаимствованием smart pointer проблем и правда не возникло. Проблемы возникли с заимствованием bind – компилятор 2005-й байнд, писаный явно для 2008-й, ниасиливал. Причем ниасиливал хитро – некоторые конструкции проблем не вызывали, но кое-где он явно не догонял, что передается указатель на параметер, а не объект, и пытался копировать интерфейс с закономерно печальным некрологом в билд логе. Не будь у нас привычки писать йунит-тесты, мы бы об этом узнали не сегодня, а где-нить через месяц. Мне до сих пор от одной мысли об этом становится несколько хреновато.

Подумали мы, порисовали фломастерами на доске и таки решили вживить в проект буст. Не весь сразу, а лишь нужные куски.

Кто работал с бустом, поймет, что это означает скачивание буста, его сборку и натравливание на него тулзень под названием bcp для вычленения нужных кусков. Нам повезло – нужные нам библиотеки header only, и собирать там нечего. Зато вот bcp собрать как раз надо было.

Вот с этим и вышли баааальшие такие деццкие грабли. Оказалось, что скрипты сборки в бусте не работают на 64-битной Винде. Не работают давно и конкретно. Там очень древний и глупый косяк, вызваный скобками в строке “C:\Program Files (x86)\”. Судя по интернетам, этот баг известен с 2006 года, и никто пока даже не почесался его исправить.

Следующие два часа поисков вариантов быстрой правки скриптов или уже собраного bcp.exe принесли твердые отрицательные результаты. Под конец дня пришлось пойти на экстренные меры – найти старую девелоперскую машину с 32-битной Виндой и собрать bcp там. Нужно ли говорить, что после выполнения этого несложного квеста все нужные заголовки были включены в проект уже через две минуты, а еще через минуту проект весело собрался и радостно отрапортовал 0 errors 0 warnings.

Ах, да, еще немного про театр абсурда: между делом я еще сегодня заставил 2010 студию использовать компилятор 2005-й. Забавный получился комбайн, надо сказать, но это уже совсем другая история.

Visual Studio 2010: Not so nice improvements in TR1

Just moments after installing VS 2010 I found very nasty bug in its implementation of TR1 libraries:

            typedef struct sTestStruct
            {
                char* pName;
                char* pOtherName;
                sTestStruct* pNext;
            } sTestStruct;

            sTestStruct Struct1 = {"blah", "mlah", 0};

            char* pChar = bind(&sTestStruct::pName, _1)(&Struct1);            // Compiler is happy here

            bind(_stricmp, "blah", bind(&sTestStruct::pName, _1))(&Struct1);  // bang happens here

The error message is simply astonishing:

\microsoft visual studio 10.0\vc\include\functional(447): error C2440: ‘return’ : cannot convert from ‘char *’ to ‘char *&’

What is interesting, should I switch C++ toolset to version 9.0 (essentially switching back to VS 2008), the problem went away.

If I could I would suggest that something went horribly wrong when MS were working on rvalues (or were they lvalues?) references business, but there is simply no point guessing around. I just wonder how long will it take MS to fix the problem. Obviously, problems like this could mean that this new version of VS 2010 is not ready to be used in production yet.

Also, reporting bugs to MS has always been not so easy quest.

Студия 2010: дневник мыши, поедающей кактус

На вкус кактус пока ничего. За прошедшие три дня студия упала три раза и два раза зависла.

Что заметно:

  • Переработали меню проекта “добавить новую хрень”.  Выглядит ново, почти вебдванольно. Эргономика, правда, не изменилась.
  • Кажется, переписали Intelli(non)Sense. Как и обещали. Работает заметно быстрее и менее косячно. Впрочем, проект у нас пока в коротких штанишках, посмотрим, что будет дальше
  • IntelliSense точно если не переписали, то сурово ковыряли. По крайней мере, в предыдущих версиях она могла ставить Студию в разные позы в случае использования C++/Cli. Типичный пример – при использовании MSTest для тестирования C++ кода Студия начинала уходить в нирвану при сколько-нибудь приличном количестве тестов в проекте. В новой версии IntelliSense просто заявляет, что для C++/Cli оно работать не будет. И, что характерно, не работает.
  • MSTest теперь по умолчанию хранит результаты лишь для 25 запусков. Я, в принципе, вообще не понимаю всей этой затеи с хранениями результатов юнит-тестов. С моей точки зрения, тесты либо прошли, либо нет. Если тесты не прошли, билд не проходит тоже. End of story. В любом случае теперь, кажется, студия перестанет захламлять диск историей тестов, которая все равно никому не нужна.
  • Хост-процесс для тестов ведет себя странно. Периодически забывает завершиться, ввиду чего пересборка DLL с тестом обламывается. Подобного заскока за прежними версиями замечено не было.
  • Отладчик при работе в Mixed mode (тот самый случай тестирования С++ кода в MSTest) ведет себя неортодоксально. Норовит подвиснуть почем зря.
  • Зачем-то сломали поведение “Project dependencies”. Теперь, чтобы подсунуть линкеру другую статическую либу из этого же солюшена, нужно проект с библиотекой добавить в рефересы проекта. Совсем как в C# проектах. Немного неожидано. Зависимости проекта оставлены лишь для определения порядка сборки.

Use boost to implement inter-class callbacks or delegates

In the software development, the pattern when one object has send a message to another object, where the object that has to be called is given to the first class somehow (usually in constructor), is quite common. In C#, the delegates serve exactly this purpose, but in C++ it is a different story.

The most common way of implementing the class-to-class callback in C++ is to define an interface, which the class that receives the event must implement:

class IInterface
{
public:
virtual void CallbackMethod() = 0;
}

class MyClass:
public IInterface
{
public:
virtual void CallbackMethod()
{
// do something useful
}

class Publisher
{
private:
IInterface* m_pCallee;
public:
Publisher(IInterface* pCallee):
m_pCallee(pCallee)
{
}

private:
void SomeMethod()
{
m_pCallee->CallbackMethod();
}
}
}

It isn’t exactly rocket science, but this method has some downsides, and some of them are quite nasty and not obvious at all. Those are:

  • You may need to define a new interface every time you need a callback function with new signature. Could partly be solved by making the interface class template
  • Each class that interested in callbacks needs to implement corresponding interface(s)
  • In some situations you may end up with a single object that can receive callbacks from many different publishers.

The last one could make you wish you never become a software programmer. The architecture, which seemed so beautiful just moments before, starts to fall into pieces once you find yourself in situation when you realize that your class-subscriber needs to receive same callbacks from different sources and act differently depending on who the caller is.

Let me explain it on the example. Imagine you have a class that implements that callback interface. You also have a lot of providers that take that interface to inform the subscribers, for example, about quote changes. But in your class, you need to track changes on, say, 10 quotes, and depending on which of those 10 changes, take different actions. So, what ’s your options?

Of course, you could just add an additional  parameter to callback method that specifies the sender, but it is not pretty at all, as the subscriber class now faces a new challenge of keeping track of all publishers it is subscribed too. Not mentioning the mess in the callback method needed to get all stuff to work!

Ideally, you want each of those providers to call different methods of your subscriber class, but in a general case it would mean 10 different interfaces and changes in all publishers, which may not be possible and I don’t even want to talk about it. The other option would be to write the proxy, which implements the basic callback interface, subscribes to the only provider and then re-routes the call to certain method on your class.

Thankfully, there is the far better option. boost::bind and boost::function are here to help.

class Publisher{
public:
typedef boost::function<void (int)> typeCallbackType;

void Subscribe(typeCallbackType cb)
{
m_cb = cb;
}

private:
typeCallbackType m_cb;

void SomeDataProcessingMethod()
{
// ....
m_cb(currentPrice);
}

}

And here is a good thing: your subscriber class does not have to implement any custom interface anymore:

class MySubscriber
{
public:
void CalledWhenPriceChanges(int newPrice)
{
// make profit here
}
}

to establish a subscription, simply use boost::bind:

Publisher* pPublisher = GetPublisherFor("someTicker");
MySubscriber Subscriber;

pPublisher->Subscribe(boost::bind(&MySubscriber::CalledWhenPriceChanges, &Subscriber, _1));

That’s all, folks!

Now, once the pPublisher has a new data, a CalledWhenPriceChanged() method of Subscriber instance of a class MySubscriber will be called. It is easy to see how additional publisher could be added if you want to monitor two tickers.

It would not only help solving the problem of multiple publishers-one subscriber. This approach will also do a great job saving you time from not having to define numbers of callback interfaces for different types of data that needs to be passed from publisher to subscriber.

Рыночная икономика, однако

Что-то Visual Assist X стоит как-то слишком много денег.

Уже с кредиткой на сайт полез, да как-то ценник всю охоту отбил.

Слов нет, вещица в хозяйстве полезная, но все же всяких ништяков оно делает как-то несравнимо меньше самой студии, которая всего в два раза дороже (вопрос, почему к Студии нужно приставлять костыли для комфортной работы, пока оставим на совести M$).

Сижу в глубокой задумчивости.

Программерское: Мой топ признаков плохого кода

Собрал тут свой собственный список признаков факапнутости кода.

На первом месте – классы со словом “Manager” в названии. Безоговорочный лидер в номинации “превратим любой код в страшные спагетти”. Самое ужасное в таких классах это то, что делаются они из самых лучших побуждений. Впрочем, так всегда – самые кошмарные деяния совершаются под прикрытием лозунга “мы же хотим тебе лучшего”. Я специально не пишу о внутренностях этих классов, само название “Manager” для меня уже достаточное основание к тому, чтобы код полетел в помойку.

Второе место достается небезопасным преобразованиям типов. Ну, это когда в коде появляется преобразование от класса-предка к классу-потомку. Вроде этого

class A;
class B: public A;

void DoSomething(A* ptr)
{
(B*)ptr->MethodOfB();
}

100% признак проявления идиотии в проектировании иерархии классов. Особый шик, когда подобный downcast встречается в одном из базовых классов – это уже индикатор того, что пора сушить весла. Исключений не бывает. Если же такой код появляется в процессе рефакторинга там, где ничего подобного до того не было, то сушить весла нужно отправлять аффтара этого рефакторинга.

Третье место – классы “универсальный всемогутор”.  Близкий родственник класса “Manager”, протащеный оным на ключевую позицию в проекте, способен отравлять жизнь двум-трем поколениям программистов, которым выпадет этот код поддерживать. Обычно характеризуются тучей публичных методов, никак не связанных по смыслу ни друг с другом, ни с названием и предназначением класса и не изменяющих и не использующих ни одного из свойств класса, но при этом зачастую имеющих тучу неявных связей друг с другом.

Четвертое место – преднамеренное нарушение энкапсуляции. Это когда в класс “бутылка пива” пытаются заодно вкрячить нечто, что вроде как должно научить экземпляры его автоматически упорядочиваться внутри класса “ящик пива”. Когда через неделю закономерно выяснится, что бутылки нужно упорядочивать в сикспаки, начинается любимый в народе аттракцион “аврал”. Есть вообще только один случай, когда объектам допустимо иметь какие-то знания о контейнере, в которые их будут складывать – это преднамеренно делается интрузивный контейнер. Во всех остальных случаях такой код – индикатор того, что code review можно прекращать прямо сейчас, иначе проблемы этот код начнет доставлять уже на следующей неделе. В общем случае – запихивание в классы внешних нерелевантных зависимостей есть большое зло.

Пятый признак – преднамеренное игнорирование или извращенное употребление паттернов программирования. Когда из-за страха перед великим и ужасным шаблоном “фабрика” образюутся иерархии, где один-единственный параметр конфигурации спускается каждым промежуточным классом вниз на десять уровней, чтобы там ему где-то делали if или switch..case, когда при программировании на C++ боятся использовать RAII или бьют по рукам за bind; или же, наоборот, пихают повсюду smart pointers (доморощенные, конечно же, бо boost – это слишком сложно), жди беды. Очень быстро кто-то посередине той иерархии забудет, зачем нужен этот мистический параметр, нигде в обозримых окрестностях не применяющийсяи “пофиксит” код, обеспечив остальных неделями отладки (ведь юнит-тесты для такого кода писать не принято – он же очень сложный!); неиспользование RAII рождает трудноуловимые дедлоки, а умные поинтеры дают вкусить циклических зависимостей по полной.

Шестой признак – использование наследования там, где нужна агрегация. Например, есть интерфейс классов, есть базовый класс, которые реализует 85% всей требуемой функциональности. Позднее выясняется, что функциональность реализована вообще на 100%, а единственное требуемое различие – это разное поведение в случае создания класса с определенными внешними параметрами. Например, в реальной жизни есть у нас автомобиль. Точнее, его кузов. Во всем мире все производители просто ставят разные двигатели на один и тот же кузов и обзывют это обидным словом “комплектация”. В мире программирования часто происходит закат разума за ум – вместо того, чтобы создать класс “автомобиль” со свойством “двигатель”, начинают городить иерархию “автомобиль А с двигателем Б”, автомобиль Б с двигателем А” и так далее, что вообще противоречит здравому смыслу. Еще бы, класс автомобиль оказывается наследником класса “двигатель”. Проблема тут в том, что разобраться во всем этом хитросплетении и потом поддерживать ее становится решительно невозможно. В то же время правильный вариант – создать нужный объект тпиа “двигатель” и передать его новому объекту типа “автомобиль” при инициализации- не только сэкономил бы пару тысяч строк кода, но и позволил бы унифицировать и протестировать все имеющиеся автомобили и двигатели отдельно друг от друга и даже попробовать впоследствии легко примерить движок от Мерседеса на Запорожец.

9 visitors online now
9 guests, 0 members
Max visitors today: 9 at 05:13 am MST
This month: 30 at 09-01-2010 12:31 am MST
This year: 41 at 01-23-2010 03:43 am MST
All time: 41 at 01-23-2010 03:43 am MST