Добавлен , опубликован

Вступление

Так как это мой первый пост, я опишу общую тематику блога. Большинство материалов будет посвящено разработке на языке программирования D2, game developing'y, в частности различным технологиям компьютерной графики. Вскоре я опубликую крупный проект, который разрабатывался в тени больше полтора года, весь материал будет косвенно связан с этим проектом. Цель моего блога - поднятие общего уровня знаний отечественных разработчиков в сфере разработки игр, а по-простому я просто хочу поделиться накопленным опытом.

Предыстория

Одним осенним вечером, а скорее ночью, я завершал разработку пакетного менеджера для клиент-серверного общения. Система позволяла определять протокол и функции, вызывающиеся удаленно (CORBA-style). Окинув взглядом мое творение, я ужаснулся. Предлагаю читателю взглянуть на пару строчек из проекта (я не прошу понимать его, просто прогляди одним глазом):
Много C++
/// Класс сообщения
/**
	@tparam TList Список формальных параметров, записанных в сообщении.
*/
template
<
	class Fun,
	class TList = Fun::inArgs
>
class Message : public AbstrMessage, public Functor < void, TList >
{
public:
	/// Стандартный конструктор для создания сообщения
	/**
		@param fun Функтор, обработчик сообщения. 
	*/
	Message( Fun fun ) :
		Functor< void, TList >(fun), iShift(sizeof(int32)), pFun(NULL)
		{
			pFun = reinterpret_cast<Fun*>(Functor< void, TList >::getFun());
		}

	~Message() { delete fun;}

	void translate(char* message);
	OutMessHandler::tempMem* form(int32 id);
	Fun* getFun() { return pFun; }

protected:
	/// Смещение в сообщении для чтения
	int iShift;
	Fun* pFun;

protected:

	#define arg(i) getVal<Parm##i>(ms, Type2Type<Parm##i>())

	/// Вызывается при обнаружении вызова этой команды
	/**
		Тут довольно сложная ситуация, есть структура с аргументами в виде строк и нужно вызвать
		нужный оператор функтора. Это достигается с помощью Int2Type, который играет здесь роль if'а.
	*/
	void translate( char* ms, Int2Type<0> argCount)
	{
		return (*this)();
	}

	/// Перегруженный аналог ConCommand::Translate( CON_COMMAND com, Int2Type<0> argCount) для 1 аргумента
	void translate( char* ms, Int2Type<1> argCount)
	{
		return (*this)( arg(1) );
	}
	/// Перегруженный аналог ConCommand::Translate( CON_COMMAND com, Int2Type<0> argCount) для 2 аргументов
	void translate( char* ms, Int2Type<2> argCount)
	{
		return (*this)( arg(1), arg(2));
	}
	/// Перегруженный аналог ConCommand::Translate( CON_COMMAND com, Int2Type<0> argCount) для 3 аргументов
	void translate( char* ms, Int2Type<3> argCount)
	{
		return (*this)( arg(1), arg(2), arg(3));
	}
	/// Перегруженный аналог ConCommand::Translate( CON_COMMAND com, Int2Type<0> argCount) для 4 аргументов
	void translate( char* ms, Int2Type<4> argCount)
	{
		return (*this)( arg(1), arg(2), arg(3), arg(4));
	}
	/// Перегруженный аналог ConCommand::Translate( CON_COMMAND com, Int2Type<0> argCount) для 5 аргументов
	void translate( char* ms, Int2Type<5> argCount)
	{
		return (*this)( arg(1), arg(2), arg(3), arg(4), arg(5));
	}
	//... and so on up to 20 arguments

	#undef arg

	/// Конвертация сырого сообщения в значение
	/**
		Берет строку и возвращает значение нужного типа.
		Поддерживаемые типы: POD типы, string.
		@note Нужны еще типы? Добавь в тело функции еще один else if.
		@attention Для неподдерживаемых типов не гарантируется стабильная работа программы =).
	*/
	template<typename T>
	T getVal( char* ms, Type2Type<T> t);

	/// Передача строки
	/**
		Строки передаются в C стиле с нулевым конечным символом.
	*/
	template< typename T >
	T getVal( char* ms, Type2Type<string> t);

};
И этот кусок я считал в модуле самым понятным, потому что были еще явные извращения вроде списков типов и type traits (не знаю корректного перевода). Тут я и задумался: кто из моей команды сможет поддерживать этот код? Почему шаблонное программирование настолько запутанное и уродливое? Может существует более правильный и красивый путь?
В течении нескольких месяцев я искал альтернативу C++ в области "generic programming", перепробовал C#, Java, Python, JavaScript. Я уже было решил писать на Lisp'е с его CLOS моделью, но тут я наткнулся на язык D...

Сильные стороны

Эффективность

Первое, что я проверил, было эффективность. Язык обладет собственным сборщиком мусора GC и у меня были сомения относительно автоматического управления памятью. Удивительно, но тесты показали, что D практически всегда быстрее C++, а на тестах с созданием множества мелких объектов, вроде векторов, обгонял в 2-3 раза (как потом я выяснил это сильная сторона любого хорошего сборщика мусора). Конечно с чистым С с включенной оптимизацией -O3 D проиграл (тесты проводились под Debian gcc), но не намного. Надо заметить, для любителей оптимизации в ди есть встроенный переносимый ассемблер.
Также меня поразила скорость компиляции, она практически мгновенная. Благодаря этому язык может использоваться как скриптовый (есть стандартный интерпретарор rdmd), что позволило мне вообще отказаться в проектах от .sh и .batch файлов и внедрить кроссплатофрменные скрипты (о кроссплатформенности ниже).

Синтаксис

С эффективностью разобрались, а что насчет синтаксического сахара? По синтаксису D наиболее близок к С и Java, на нем можно сходу писать небольшие программы, даже не подозревая, что это D. Я не смогу рассмотреть все, поэтому отмечу самые важные моменты.
Не говоря уже о детской радости от использования вменяемого foreach (В других языках это уже давно), D использует технологию принципиально противоположную итераторам: Ranges. Благодаря очень мощным встроенным массивам, взятие подстроки реализуется очень просто:
	string s = "some awesome string";
	...
	s = s[6..$];
	assert(s == "awesome string"); // Думаю ассерты знакомы многим, о поддержке обработки ошибок ниже
При этом не создается новых массивов, в D массив является просто паком из двух указателей на начало и конец массива, что позволяет отлавливать выход за пределы и делать вот такой "slicing". Нужно упомянуть, что язык изначально поддерживает Unicode, но и имеет инструменты для работы с байтами. Вернемся к foreach и концепции Ranges, пример обработки введенного текста пословно:
import std.stdio;
import std.algorithm;

void main() 
{
    foreach (line; stdin.byLine()) 
		foreach(word; splitter(line, " "))
		{
			writeln(word);
		}
}
D2 имеет очень мощную и хорошо документированную стандартную библиотеку Phobos (в эпоху D1 их было две, но это уже история), благодаря которой прикладные программки пишутся легко и с удовольствием.
В отличии от нового стандарта C++ в D2 есть полноценные лямбды с замыканиями и вывод типов, пример объяснит лучше:
	void someFunc(double a1, dobule a2)
	{
		// Типы выводятся на этапе компиляции
 		auto temp = new int[5];
		// здесь мы передаем лямбду некой функции, которая будет использовать ее позже
		sendAlgorithm(
			(double arg) // возвращаемые типы выводятся автоматически
			{
				// мы имеем доступ к переменным, где была создана лямбда
				writeln(a1,"+",a2,"?=",arg);
				return a1+a2 == arg;
			});
	}

Обработка ошибок

Язык имеет очень много инструментов для обработки ошибок, начиная от привычных исключений с try/catch/finally блоками, заканчивая встроенными юнит-тестами и контрактным программированием. Рассмотрим примеры:
// Две абсолютно одинаковых функций с классической обработкой исключений и scope выражением:
void sendFile(Stream file)
{
	try
	{
		// Выдуманная функция, которая кидает исключение
		sendByBlocks(file);
	} catch(Exception e)
	{
		writeln("Передача файла не удалась!");
	} finally
	{
		// Вообще файл сам закроется, как только выйдет из зоны видимости, но это пример
		file.close();
	}
}

void sendFile(Stream file)
{
	scope(failure) // есть также success, который выполняется при отсутствии исключений
	{
		writeln("Передача файла не удалась!");
		file.close();
	}
	sendByBlocks(file);
}
Практически везде можно объявить блок с юнит-тестами и провести доскональное испытание вашего кода:
unittest // В релизную версию тесты не попадают
{
	// Ассерты позволяют легко проверить работоспособность функции и вывести корретное сообщение об ошибке
	assert(myFunc() == expectedResult, "Тест моей функции провален!");
}
Также очень полезно контрактное программирование, когда функция представляется в виде трех блоков: in, body, out. in и out проверяют входные и выходные параметры функции, очень полезно для интерфейсов, где объявлются только in out блоки. Все контракты вырезаются из релизной сборки приложения для ускорения работы.
// В языке отсутствует множественное наследование классов, но можно наследовать много интерфейсов как в Java
interface SomeInterface
{
	double someFunc(double a1, double a2)
	in
	{
		// Проверяем входные данные
	}
	out(result)
	{
		// Проверяем выходные данные
	}
}

Обощенное программирование

Теперь самое вкусное. В D расширили концепцию программирования на этапе компиляции, теперь любая функция с модификатором static может выполняться во время компиляции, а каждая функция имеет два! списка параметров, один ей передается в compiletime, другой в runtime.
	// Тип T передается во время компиляции
	// ref обозначет, что аргументы переданы по ссылке
	void swap(T)(ref T a1, ref T a2)
	{
		T temp = a1;
		a1 = a2;
		a2 = a1;
	}
Также улучшена перегрузка обобщенных функций, теперь вместо придумывания всяких костылей можно делать вот так:
	import std.traits;

	// Если guard выражение вернет false, то эта функция даже не рассматривается как кандидат для вызова
	string convert(T)(T arg) 
		if( isFloatingPoint!T )
	{
		//...
	}

	// Эта перегрузка будет вызвана только для класса myClass
	string convert(T)(T arg)
		if( is( T == myClass) )
	{
		//...
	}
Хотя множественное наследование классов запрещено в D. Есть очень интересная возможность делать "примеси" (классические mixin из Scala):
// Эта функция может выполняться в compiletime
static string constructField(T)(string name)
{
	return T.stringof~" m"~name~";";
}

class MyClass
{
	// mixin внедряет строку как код на этапе компиляции
	// Результат: double mTime;
	// Знак "!" используется для указания compiletime аргументов
	mixin constructField!(double)("Time");
}
В стандартной библиотеке есть множество функций для получения списка типов параметров функции, списка всех методов класса, всех наследников класса и тому подобное, это то, чего мне жизненно не хватало в С++. Также ди поддерживает безопасные variadic функции:
// В функцию можно передать сколько угодно интов, все они упакуются в аккуратный массив
int func1(int[] args...)
{
	int temp; // у каждого типа есть свойство init, которым инициализируется переменная
	foreach(arg; args)
		temp += arg;
	return temp;
}

// Это уже интереснее, в функцию можно передать сколько угодно различных аргументов различных типов
// все будет упаковано в специальный массив, такую нотацию использует writeln
int func2(T...)(T args...)
{
	foreach(i,arg; args)
	{
		// Типы аргументов тоже упакованы в массив
		writeln(i, " type: ", T[i].stringof, " ", arg);
	}
}

Многопоточность

Язык перенял все положительные тенденции в современном многопоточном программировании. Используется модель языка Erlang, все потоки изолированы (даже имеют по своей копии всех глобальных переменных) и общение происходит через посылку ассинхронных сообщений. Также используются immutable типы, которые гарантированно никогда не меняются, например строки это immutable(char[]).
Однако можно явно объявить некоторые классы или переменные с модификатором shared, и компилятор будет вас доставать, когда идет явно ошибочное использование расшаренных данных, приводящее к дедлоками или к гонкам. Можно добавить к классу модификатор synchronized (Java подход) и к объекту будет привязан свой мьютекс, который следит за блокировками при входе и выходе из методов.
До D я не знал о существовании такого подхода, как locking-free programming, когда используются встроенные процессорные атомарные примитивы (check and set = cas), позволяющие вообще отказаться от блокировок, но это воистинну очень сложная техника (гораздо легче использовать immutable).

Кроссплатформенность

Она предоставляется из коробки. Любой платформозависимый код можно обернуть в специальные блоки version, в каждом из которых находится код для определенной платформы:
version(Windows)
{
	...
}
version(linux)
{
	...
}
version(MacOS)
{
	...
}
// Меток версий ос просто уйма, можно посмотреть на офф. сайте
// также блоки версий можно использовать со своими константами для поддержки различных версий своего же софта
Стандартная библиотека кроссплатформенна, большинство существующих библиотек под D кроссплатформенные. Писать платформонезависимый код в D намного удобнее и быстрее чем в других языках.
Однако есть проблема с gui библиотекой, единственной вменяемой является порт gtk.

Все эти возможности открывают невиданные просторы для экспериментов и улучшения своего кода, при этом сохраняется читаемость и эффективность. В D есть еще несколько более хитрых фишечек вроде встроенная статическая диспетчиризация (при вызове несуществующего метода класса, компилятор вызывает специальны оператор в классе и передает ему строку-название метода). Тех, кто дочитал до этого момента, я искренне благодарю. Подробнее о языке можно узнать на оффициальном сайте(dlang.org) и из книги А.Александреску "The D Programming Language" (не знаю, появился ли перевод на русский). Далее пойдет речь о недостатках и проблемах языка.

Недостатки

  • Первое, что замечаешь при переходе с C++, что D имеет довольно ограниченный инструментарий для взаимодействия с C++, а точнее D бинарно совместим только с C и тонны собственных библиотек приходится портировать на D вручную, однако я заметил тенденцию, что после портирования код на 50% компактнее и прекрастно читается.
  • Отсутствие нормальных IDE, это удар ниже пояса. Язык еще очень молод и среды разработки все еще в альфа-бета версиях. Есть плагин для Visual Studio называемый VisualD, но автодополнение и анализ кода там очень хромают, есть D-IDE на .net - очень многообещающая среда, но сырая и только под Windows, есть DDT плагин для Eclipse с тяжеловесной проверкой синтаксиса, но у него довольно топорные настройки, которые мне не подошли. Однако вскоре понимаешь, что этот язык страдает не так сильно от отсутсвия IDE, сейчас пользуюсь Sublime Text 2 и я удовлетворен полностью, все сложности с поиском функций,классов и т.д. отошли к организации структуры сорцов и хорошей документации.
  • Под виндой есть просто подводная гора, компилятор dmd гененрирует obj файлы в формате OMF, несовместимый с майкрософтовским COFF форматом. Поэтому у меня не получилось подключить, например, CUDA к проекту, потому что компилятор от Nvidia генерирует как раз COFF объектные файлы. Никакие ухищрения и конверторы не помогли. Под другими платформами такой проблемы вообще нет, так как линкует все файлы gcc. Из-за этого же различия форматов могут быть странные проблемы под виндой при передаче функций в сишные библиотеки, вплоть до падений приложения.
  • Отсутствие огромного количества библиотек, однако все pure С и предоставляющие C интерфес библиотеки подходят для использования. На D все еще не существует хоть какого-нибудь трехмерного графического движка (что я пытаюсь исправить).
  • Недавно я столкнулся с багом компилятора (что очень плохо) при генерирования shared library (.so) под 64 битную архитектуру, хоть все исходники компилятора и стандартной библиотеки открыты, существующие компиляторы C++, С, С# и т.д. гораздо стабильнее.
  • Высокий порог вхождения. Язык полон различных фич и инструментов, не навязывает определенную парадигму программирования (я не рассказал о функционально программировании в D), поэтому требует от программиста четкого понимания, чего он хочет. Поэтому я бы не советовал язык новичкам.

Подведя итоги, мое мнение: D отличный язык как для системного, так и для прикладного программирования, но как технология еще довольно молодая и страдающая детскими проблемами. Сообщество вокруг языка маленькое, но активное. Я буду очень рад, если мой пост хоть как-то поможет развитию языка.
0
29
12 лет назад
0
Пока не прочел, но D мне тоже нравится, единственная проблема - 2 стандарт либы. А так - вроде неплохая альтернатива плюсам и шарпу.
А, D2. Не слышал про него, на вики этого не было. Ну и отлично.
А вот такой вопрос, у тебя строка:
s = s[6..$];
$ - знак конца строки в regexp. Можно ли в этой конструкции использовать регулярку?
Годный синтаксис. Действительно смей плюсов, шарпа и жавы.
(даже имеют по своей копии всех глобальных переменных)
По дефолту? В жаве это определялось атрибутом volatile.
В общем, прекрасный пост, интересно было почитать.
Думаю, попробую Ди, когда появятся нормальные IDE. (Лучше всего нормального плагина на эклипс)
Как я понял, OpenGL не поддерживается. Печально, жду поддержки. Ясно что язык пока молод =)
Пост может быть немного несвязный, писал по мере чтения.
0
18
12 лет назад
0
  1. В D2 одна либа Phobos, D1 постепенно отдаляется на второй план, его вторую либу Tango так и не портировали на D2.
  2. Знак $ в конце - оператор, который возвращает длину массива, его можно перегружать и используется именно в slicing. Регулярки перловские, находятся в модуле std.regex, пробовал, очень удобно.
  3. По дефолту все потоки имеют копию окружения, все static поля и глобальные переменные для них копируются и хранятся в TLS (Thread Local Storage), что немного замедляет доступ. Есть модификатор __gcshared, такой бекдор, позволяющий сделать переменную глобальной для всех потоков как в других языках, но компилятор не будет гарантировать защиту от гонок и неправильного использования.
  4. OpenGL поддерживается полностью, мой следующий пост будет про его подключение.
  5. Я связался с разрабом D-IDE, вместе фиксим баги, которые появляются в больших проектах.
D2 релизнулся совсем недавно, весной прошлого года
Можно ли в этой конструкции использовать регулярку?
В слайсинге нельзя пользоваться регулярками, там нужно явно указывать начало и конец среза. Регулярки в основном для строк используются, для других массивов их нельзя применять. (Как я посмотрел в std.regex стоят guard выражения isSomeString!T)
Чтобы оставить комментарий, пожалуйста, войдите на сайт.