If a C programmer asks "do you want to see something cool?", run away.
--John Van Enk

Thursday, May 15, 2008

Designing and Building Portable Systems in C++. Part I - Problems

В прошлом посте я запостил ссылку на документ, опсывающий некоторые тонкости, разработки портируемых программ на C++. Наконец дошли руки более подробно прочитать этот документ и я с удивлением обнаружил, что на прошлой работе с некоторыми описываемыми ситуациями мне приходилось сталкиваться. Несмотря на то, что сам язык был спроектирован как платформонезависимый, создание кроссплатформенного ПО не является таким простым, как хотелось бы. Однако обо всем по порядку...

Одной из основных проблем, при написании портируемого кода на С++, являются отличия компиляторов. Несмотря на то, что сам язык описан довольно детально в стандарте, трактовки самого стандарта в станах компиляторостроителей иногда различаются. Кроме того, C++ часто ругают (вполне заслуженно) за сложность, особенно когда дело касается шаблонов. Эта сложность по определению ну никак не может облегчить жизнь как разработчикам компиляторов, так и "обычным" программистам, которые эти компиляторы используют.

Однако не только сложность языка источник проблем. Стандарт - один из этих источников. Описывая поведение большинства (но не всех) элементов языка, он, вместе с тем, оставляет некоторые из них implementation-defined, unspecified или undefined. В тексте документа приводится такой пример:
void f(int a, int b, int c)
{
cout << "a = " << a << ", b = " << b << ", c = " << c << endl;
}

int main(int argc, char** argv)
{
int i = 0;
f(i++, i++, i++);
return 0;
}

И оговаривается, что на разных платформах результат выполнения программы будет разным

For example, on Mac OS X, using GCC 3.3, the program fragment yields the following output
a = 0, b = 1, c = 2

This is what one would intuitively expect, assuming left-to-right argument evaluation. Compiled with HP ANSI C++ A.03.57 on HP-UX 11.11, the result is the same. However,when compiled with Compaq C++ 6.5 on HP Tru64 5.1, the program yields:
a = 0, b = 0, c = 0

which may be surprising

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

"Расширения" языка, которыми зачастую грешат разработчики из обоих лагерей (MS и GNU), тоже мешают писать портируемый код. Особенно "грешат" на майкрософтовский компилер, у которого есть опция для отключения подобного рода расширений. Заодно автоматически у вас пропадает возможность использовать Platform SDK, так как его заголовочные файлы используют эти расширения. Меня самого когда-то возмущала необходимость использовать тип BOOL, вместо bool (сейчас уже правда не возмущает) и я до сих пор иногда забываю ставить новую строчку в конце файла, с раздражением наблюдая ворнинги компилятора.

Типы. На разных платформах фундаментальные типы могут иметь разный размер. Поэтому если в вашей программе необходимо 32 битное беззнаковое целое, то проще всего, наверное, определить свой тип в заголовочном файле сразу, чем потом по всему коду выискивать и менять unsigned на myint32_t. В стандарте языка С ISO/IEC 988:1999 к стати есть типы int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t, определенные в stdint.h, однако это тот случай, где С++ не обратно совместим с С. Может в следующем стандарте поправят. Там вроде как намечается соласовать стандарты. Про типы с плавающей запятой вообще отдельная сказка, ее лучше почитать в оригинале.

Порядок байт. Определяет как размещаются байты в памяти - старший->младший или младший->старший. Это потенциальная проблема, если есть необходимость производить обмент бинарными данными между системами с различным порядком байт.

Выравнивание данных. На многих архитектурах данные обычно выравниваются на границу слова, что иногда может приводить к проблемам. В частности, приводится следующий пример:
struct Header
{
UInt32 size;
UInt32 checksum;
};
...
void handleData(void* pData)
{
Header* pHeader = reinterpret_cast<Header*>(pData);
for (int i = 0; i < pHeader->size; ++i)
...
}

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

Там где дело касается API операционных систем дела обстоят еще хуже - совсем недавно в одном из блогов наткнулся на замечания по поводу портирования софта linux -> windows (блог к стати мне понравился, жаль ссылку потерял). Для меня, к стати, оказалась сюрпризом информация о том, что Windows NT поддерживала POSIX, а с приходом WindowsXP ее убрали.

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