Каждый С-программист с опытом накапливает привычный багаж техник и идиом. Зачастую бывает сложно понять, как сделать то же самое в новом языке. Так вот, вашему вниманию предлагается коллекция распространенных паттернов на C и их эквивалентов на D. Если вы собираетесь перевести свою программу с C на D или ещё сомневаетесь стоит ли это делать, то эта статья для вас.
Получаем размер типа в байтах
В C используем специальную функцию:
sizeof( int )
sizeof( char * )
sizeof( double )
sizeof( struct Foo )
В D у каждого типа есть специальное свойство:
int.sizeof
(char*).sizeof
double.sizeof
Foo.sizeof
Получаем максимальное и минимальное значение типа
Было на C:
#include <limits.h>
#include <math.h>
CHAR_MAX
CHAR_MIN
ULONG_MAX
DBL_MIN
Стало на D:
char.max
char.min
ulong.max
double.min
Таблица соответствия типов C => D
bool => bool
char => char
signed char => byte
unsigned char => ubyte
short => short
unsigned short => ushort
wchar_t => wchar
int => int
unsigned => uint
long => int
unsigned long => uint
long long => long
unsigned long long => ulong
float => float
double => double
long double => real
_Imaginary long double => ireal
_Complex long double => creal
Особые значения чисел с плавающей точкой
Было на C:
#include <fp.h>
NAN
INFINITY
#include <float.h>
DBL_DIG
DBL_EPSILON
DBL_MANT_DIG
DBL_MAX_10_EXP
DBL_MAX_EXP
DBL_MIN_10_EXP
DBL_MIN_EXP
Стало на D:
double.nan
double.infinity
double.dig
double.epsilon
double.mant_dig
double.max_10_exp
double.max_exp
double.min_10_exp
double.min_exp
Остаток от деления вещественных чисел
В C используем специальную функцию:
#include <math.h>
float f = fmodf( x , y );
double d = fmod( x , y );
long double r = fmodl( x , y );
D имеет специальный оператор для этой операции:
float f = x % y;
double d = x % y;
real r = x % y;
Обработка NaN значений
В C сравнение с NaN является неопределённым поведением и разные компиляторы по-разному реагируют (от игнорирования до возбуждения исключения), поэтому приходится использовать специальные функции:
#include <math.h>
if( isnan( x ) || isnan( y ) ) {
result = FALSE;
} else {
result = ( x < y );
}
В D сравнение с NaN — всегда возвращает false:
result = ( x < y ); // false if x or y is nan
Асерты — полезный механизм выявления ошибок
В C нет встроенного механизма асертов, но он поддерживает псевдоконстанты __FILE__, __LINE__ и макросы, с помощью которых можно реализовать асерты (по факту, у этих констант нет другого практического применения):
#include <assert.h>
assert( e == 0 );
D поддерживает асерты на уровне языка:
assert( e == 0 );
Итерирование по массиву
На C в задаёте длину массива константой, а потом пробегаетесь по массиву громоздким for-циклом:
#define ARRAY_LENGTH 17
int array[ ARRAY_LENGTH ];
for( i = 0 ; i < ARRAY_LENGTH ; i++ ) {
func( array[i] );
}
Вы также можете использовать неуклюжее выражение с sizeof(), но это не сильно меняет дело:
int array[17];
for( i = 0 ; i < sizeof( array ) / sizeof( array[0] ) ; i++ ) {
func( array[i] );
}
В D у массивов есть свойство length:
int array[17];
foreach( i ; 0 .. array.length ) {
func( array[i] );
}
Но, если есть возможность, лучше использовать foreach-цикл:
int array[17];
foreach( value ; array ) {
func( value );
}
Инициализация элементов массива
На C вы вынуждены были пробегаться по массиву в цикле (или опять же использовать макрос):
#define ARRAY_LENGTH 17
int array[ ARRAY_LENGTH ];
for( i = 0 ; i < ARRAY_LENGTH ; i++ ) {
array[i] = value;
}
D имеет специальную простую нотацию для этого частого случая:
int array[17];
array[] = value;
Создание массивов переменной длины
C не поддерживает такие массивы, поэтому приходится заводить отдельную переменную для длины вручную управлять выделением памяти:
#include <stdlib.h>
int array_length;
int *array;
int *newarray;
newarray = (int *) realloc( array , ( array_length + 1 ) * sizeof( int ) );
if( !newarray ) error( "out of memory" );
array = newarray;
array[ array_length++ ] = x;
D имеет встроенную поддержку массивов переменной длины и сам обеспечивает правильную работу с памятью:
int[] array;
int x;
array.length = array.length + 1;
array[ array.length - 1 ] = x;
Соединение строк
На C приходится решать множество проблем типа «когда память может быть освобождена», «как обрабатывать нулевые указатели», «как узнать длину строки», «сколько памяти выделить» и другие:
#include <string.h>
char *s1;
char *s2;
char *s;
// Concatenate s1 and s2, and put result in s
free(s);
s = (char *) malloc( ( s1 ? strlen( s1 ) : 0 ) + ( s2 ? strlen( s2 ) : 0 ) + 1 );
if( !s ) error( "out of memory" );
if( s1 ) {
strcpy( s, s1 );
} else {
*s = 0;
}
if( s2 ) {
strcpy( s + strlen( s ) , s2 );
}
// Append "hello" to s
char hello[] = "hello";
char *news;
size_t lens = s ? strlen( s ) : 0;
news = (char *) realloc( s , ( lens + sizeof( hello ) + 1 ) * sizeof( char ) );
if( !news ) error( "out of memory" );
s = news;
memcpy( s + lens , hello , sizeof( hello ) );
В D есть специальные перегружаемые операторы ~ и ~= предназначенные для соединения списков:
char[] s1;
char[] s2;
char[] s;
s = s1 ~ s2;
s ~= "hello";
Форматированный вывод
В C основной способ форматированного вывода — это функция printf():
#include <stdio.h>
printf( "Calling all cars %d times!n" , ntimes );
Что мы напишем в D? Да почти то же самое:
import std.stdio;
writefln( "Calling all cars %s times!" , ntimes );
Но в отличие от printf, writef типобезопасен, то есть компилятор проверит соответствие типов переданных параметров типам в шаблоне.
Обращение к функциям до объявления
В C компилятор не позволяет обращаться к функции до того, как встретил её объявление, поэтому приходится либо переносить саму функцию, либо, если перенос не возможен, то вставлять специальную декларацию, говорящую компилятору, что функция будет объявлена позже:
void forwardfunc();
void myfunc() {
forwardfunc();
}
void forwardfunc() {
...
}
Компилятор D анализирует файл целиком, при этом игнорирует порядок следования объявлений в исходниках:
void myfunc() {
forwardfunc();
}
void forwardfunc() {
...
}
Функции без аргументов
Было на C:
void foo( void );
Стало на D:
void foo() {
...
}
Выход из нескольких блоков кода
В C операторы break и continue позволяют выйти лишь на один уровень вверх. Чтобы выйти сразу из нескольких блоков кода, приходится использовать goto:
for( i = 0 ; i < 10 ; i++ ) {
for( j = 0 ; j < 10 ; j++ ) {
if( j == 3 ) goto Louter;
if( j == 4 ) goto L2;
}
L2:;
}
Louter:;
В D вы можете пометить блок кода и затем выйти из него с любой глубины вложенности:
Louter: for( i = 0 ; i < 10 ; i++ ) {
for( j = 0 ; j < 10 ; j++ ) {
if (j == 3) break Louter;
if (j == 4) continue Louter;
}
}
// break Louter goes here
Пространство имён структур
В C несколько напрягает, что у структур отдельное пространство имён, из-за чего каждый раз перед именем структуры приходится указывать ключевое слово struct. Поэтому, типичный способ объявления структур выглядит так:
typedef struct ABC { ... } ABC;
В D ключевое слово struct используется для объявления структур в том же пространстве имён, что и все остальные объявления, так что достаточно писать просто:
struct ABC { ... }
Ветвление по строковым значениям (например, обработка аргументов командной строки)
На C вы вынуждены заводить для этого массив строк, синхронный с ним список констант, последовательно итерироваться по массиву в поисках нужной строки, а потом делать switch-case по этим константам:
#include <string.h>
void dostring( char *s ) {
enum Strings { Hello, Goodbye, Maybe, Max };
static char *table[] = { "hello", "goodbye", "maybe" };
int i;
for( i = 0 ; i < Max ; i++ ) {
if( strcmp( s , table[i] ) == 0 ) break;
}
switch( i ) {
case Hello: ...
case Goodbye: ...
case Maybe: ...
default: ...
}
}
При большом числе вариантов становится сложно поддерживать синхронность этих трёх структур данных, что ведёт к ошибкам. Кроме того, последовательный перебор вариантов — не слишком эффективен при большом их числе, а значит требуется ещё более сложный код, чтобы искать не линейно, а, например, двоичным поиском или через хеш таблицу.
D же расширяет функционал switch в том числе и на строки, что упрощает исходный код и позволяет компилятору сгенерировать наиболее оптимальный машинный код:
void dostring( string s ) {
switch( s ) {
case "hello": ...
case "goodbye": ...
case "maybe": ...
default: ...
}
}
Выравнивание полей структур
В C управление выравниванием происходит через аргументы компилятора и влияет сразу на всю программу и боже упаси вас не перекомпилировать какой-нибудь модуль или библиотеку. Для решения этой проблемы используются директивы препроцессора #pragma pack, но директивы эти не портабельны и сильно зависят от используемого компилятора:
#pragma pack(1)
struct ABC {
...
};
#pragma pack()
В D есть специальный синтаксис, с помощью которого вы можете детально настроить как выравнивать те или иные поля (По умолчанию поля выравниваются в совместимой с C манере):
struct ABC {
int z; // z is aligned to the default
align(1) int x; // x is byte aligned
align(4) {
... // declarations in {} are dword aligned
}
align(2): // switch to word alignment from here on
int y; // y is word aligned
}
Анонимные структуры и объединения
C требует всем структурам давать имена, даже если они излишни:
struct Foo {
int i;
union Bar {
struct Abc { int x; long y; } _abc;
char *p;
} _bar;
};
#define x _bar._abc.x
#define y _bar._abc.y
#define p _bar.p
struct Foo f;
f.i;
f.x;
f.y;
f.p;
Этот код не просто громоздкий, но и с использованием макросов для инкапсуляции внутренней структуры, что приводит к тому, что символьный отладчик не понимает что тут происходит, да ещё и макросы эти имеют глобальную область видимости, а не ограничены одной лишь структурой.
D поддерживает анонимные структуры, что позволяет выражать вложенные сущности более естественным образом:
struct Foo {
int i;
union {
struct { int x; long y; }
char* p;
}
}
Foo f;
f.i;
f.x;
f.y;
f.p;
Определение структур и переменных
На C вы можете объявить и структуру и переменную одним выражением:
struct Foo { int x; int y; } foo;
Или по отдельности:
struct Foo { int x; int y; }; // note terminating ;
struct Foo foo;
В D всегда используются отдельные выражения:
struct Foo { int x; int y; } // note there is no terminating ;
Foo foo;
Получение смещения поля структуры
В C, опять же, используются макросы:
#include <stddef>
struct Foo { int x; int y; };
off = offsetof( Foo , y );
В D у каждого поля есть специальное свойство:
struct Foo { int x; int y; }
off = Foo.y.offsetof;
Инициализация объединений
В C инициализируется первое подходящее по типу поле, что может приводить к скрытым багам при изменении их состава и порядка:
union U { int a; long b; };
union U x = { 5 }; // initialize member 'a' to 5
В D вам необходимо явно указать какому полю вы присваиваете значение:
union U { int a; long b; }
U x = { a : 5 };
Инициализация структур
В C поля инициализируются в порядке их объявления, что не является проблемой для маленьких структур, но становится настоящей головной болью в случае структур больших, а также в случаях, когда необходимо изменить порядок следования и состав полей:
struct S { int a; int b; int d; int d; };
struct S x = { 5 , 3 , 2 , 10 };
В D вы тоже можете инициализировать поля по порядку, но лучше всё же явно указывать имена инициализируемых полей:
struct S { int a; int b; }
S x = { b : 3 , a : 5 };
Инициализация массивов
В C массивы инициализируются по порядку следования элементов:
int a[3] = { 3 , 2 , 2 };
Вложенные массивы в C могут не окружаться фигурными скобками:
int b[3][2] = { 2,3 , { 6 , 5 } , 3,4 };
В D, разумеется, элементы инициализируются также по порядку, но вы можете и явно указывать смещения. Следующие объявления приводят к одному и тому же результату:
int[3] a = [ 3, 2, 0 ];
int[3] a = [ 3, 2 ]; // unsupplied initializers are 0, just like in C
int[3] a = [ 2 : 0, 0 : 3, 1 : 2 ];
int[3] a = [ 2 : 0, 0 : 3, 2 ]; // if not supplied, the index is the previous one plus one.
Явное указание индексов очень полезно, когда в качестве смещений необходимо иметь значение из какого-либо набора:
enum color { black, red, green }
int[3] c = [ black : 3, green : 2, red : 5 ];
Скобки для вложенных массивов обязательны:
int[2][3] b = [ [ 2 , 3 ] , [ 6 , 5 ] , [ 3 , 4 ] ];
int[2][3] b = [ [ 2 , 6 , 3 ] , [ 3 , 5 , 4 ] ]; // error
Экранирование спецсимволов в строках
В C проблемно использовать символ обратной косой черты, так как он означает начало специальной последовательности, поэтому его необходимо дублировать:
char file[] = "c:\root\file.c"; // c:rootfile.c
char quoteString[] = ""[^\\]*(\\.[^\\]*)*""; // /"[^\]*(\.[^\]*)*"/
В D в дополнение к обычным строкам с экранированием в стиле C, есть и так называемые «сырые строки», где экранирование не работает, и вы получаете ровно то, что ввели:
string file = r"c:rootfile.c"; // c:rootfile.c
string quotedString = `"[^\]*(\.[^\]*)*"`; // "[^\]*(\.[^\]*)*"
ASCII против многобайтных кодировок
В C используется отдельный тип символов wchar_t и специальный префикс L у строковых констант:
#include <wchar.h>
char foo_ascii[] = "hello";
wchar_t foo_wchar[] = L"hello";
Но из-за этого есть проблема с написанием универсального кода, совместимого с разными типами символов, что решается специальными макросами, добавляющими необходимые конвертации:
#include <tchar.h>
tchar string[] = TEXT( "hello" );
Компилятор D выводит типы констант из контекста использования, снимая с программиста бремя указывать типы символов вручную:
string utf8 = "hello"; // UTF-8 string
wstring utf16 = "hello"; // UTF-16 string
dstring utf32 = "hello"; // UTF-32 string
Однако, есть и специальные суффиксы, указывающие тип символов строковых констант:
auto str = "hello"; // UTF-8 string
auto _utf8 = "hello"c; // UTF-8 string
auto _utf16 = "hello"w; // UTF-16 string
auto _utf32 = "hello"d; // UTF-32 string
Отображение перечисления на массив
В C вы отдельно объявляете перечисление, отдельно массив, что довольно сложно поддерживать, когда число элементов разрастается:
enum COLORS { red , blue , green , max };
char *cstring[ max ] = { "red" , "blue" , "green" };
В D такое отображение задаётся парами ключ-значение, что гораздо проще в поддержке:
enum COLORS { red, blue, green }
string[ COLORS.max + 1 ] cstring = [
COLORS.red : "red",
COLORS.blue : "blue",
COLORS.green : "green",
];
Создание новых типов
В C оператор typedef на самом деле создаёт не новый тип, а всего лишь псевдоним:
typedef void *Handle;
void foo( void * );
void bar( Handle );
Handle h;
foo( h ); // coding bug not caught
bar( h ); // ok
При этом, для задания значения по умолчанию, приходится использовать макросы:
#define HANDLE_INIT ( (Handle) -1 )
Handle h = HANDLE_INIT;
h = func();
if( h != HANDLE_INIT ) {
...
}
Чтобы в C реально создать новый тип, с которым будет работать как проверка типов, так и перегрузка функций, необходимо создать создать структуру:
struct Handle__ { void *value; }
typedef struct Handle__ *Handle;
void foo( void * );
void bar( Handle );
Handle h;
foo( h ); // syntax error
bar( h ); // ok
А работа со значениями по умолчанию превращается в чёрную магию:
struct Handle__ HANDLE_INIT;
// call this function upon startup
void init_handle() {
HANDLE_INIT.value = (void *)-1;
}
Handle h = HANDLE_INIT;
h = func();
if( memcmp( &h , &HANDLE_INIT , sizeof( Handle ) ) != 0 ) {
...
}
D же обладает мощными возможностями метапрограммирования, что позволяет реализовать typedef самостоятельно и подключать из библиотеки:
import std.typecons;
alias Handle = Typedef!( void* );
void foo( void* );
void bar( Handle );
Handle h;
foo( h ); // syntax error
bar( h ); // ok
Вторым параметром шаблона Typedef можно указать значение по умолчанию, которое и попадёт в стандартное свойство всех типов — init:
alias Handle = Typedef!( void* , cast( void* ) -1 );
Handle h;
h = func();
if( h != Handle.init ) {
...
}
Сравнение структур
На C нет простого способа сравнить две структуры, поэтому приходится использовать сравнение диапазонов памяти:
#include <string.h>
struct A x , y;
...
if( memcmp( &x , &y , sizeof( struct A ) ) == 0 ) {
...
}
Отсутствие проверки типов оказывается не самой серьёзной проблемой этого кода. Дело в том, что поля структуры хранятся выровненными по границам машинного слова из соображений производительности, но компилятор C не гарантирует, что в промежутках между полями не будет мусора, оставшегося от ранее хранящихся в том же месте памяти данных, что приведёт к тому, что вроде бы одинаковые структуры признаются различными.
В D вы просто сравниваете значения, а компилятор обо всём позаботится:
A x , y;
...
if( x == y ) {
...
}
Сравнение строк
В C используется специальная функция, которая последовательно сравнивает байты до нулевого байта, которым заканчиваются все строки:
char str[] = "hello";
if( strcmp( str , "betty" ) == 0 ) { // do strings match?
...
}
В D же вы просто используете стандартный оператор сравнения:
string str = "hello";
if( str == "betty" ) {
...
}
Строка в D являются не более чем массивом символов, перед которым сохранена его длина, что позволяет сравнивать строки с гораздо большей эффективностью, посредством сравнения диапазонов памяти. Более того D поддерживает и операции отношения в отношении строк:
string str = "hello";
if( str < "betty" ) {
...
}
Сортировка массивов
Хоть многие C программисты и велосипедят из раза в раз пузырьковые сортировки, правильный путь — это использовать библиотечную функцию qsort():
int compare( const void *p1 , const void *p2 ) {
type *t1 = (type *) p1;
type *t2 = (type *) p2;
return *t1 - *t2;
}
type array[10];
...
qsort( array , sizeof( array ) / sizeof( array[0] ), sizeof( array[0] ), compare );
К сожалению, функция compare() должна быть объявлена явно и подходить к сортируемым типам.
D имеет мощную библиотеку алгоритмов, работающую как со встроенными, так и с пользовательскими типами:
import std.algorithm;
type[] array;
...
sort( array ); // sort array in-place
array.sort!"a>b" // using custom compare function
array.sort!( ( a , b ) => ( a > b ) ) // same as above
Строковые литералы
C не поддерживает многострочные строковые константы, однако с помощью экранирования перевода строки можно добиться их подобия:
"This text "spans"n
multiplen
linesn"
В D экранировать необходимо лишь кавычки, что позволяет вставлять текст в исходники практически как есть:
"This text "spans"
multiple
lines
"
Обход структур данных
Рассмотрим простую функцию поиска строки в бинарном дереве. В C мы вынуждены создать вспомогательную функцию membersearchx, которая используется для непосредственно обхода дерева. Чтобы она не просто ходила, но и делала что-то полезное мы передаём ей ссылку на контекст в виде специальной структуры Paramblock:
struct Symbol {
char *id;
struct Symbol *left;
struct Symbol *right;
};
struct Paramblock {
char *id;
struct Symbol *sm;
};
static void membersearchx( struct Paramblock *p , struct Symbol *s ) {
while( s ) {
if( strcmp( p->id , s->id ) == 0 ) {
if( p->sm ) error( "ambiguous member %sn" , p->id );
p->sm = s;
}
if( s->left ) {
membersearchx(p,s->left);
}
s = s->right;
}
}
struct Symbol *symbol_membersearch( Symbol *table[] , int tablemax , char *id ) {
struct Paramblock pb;
int i;
pb.id = id;
pb.sm = NULL;
for( i = 0 ; i < tablemax ; i++ ) {
membersearchx( pb , table[i] );
}
return pb.sm;
}
В D всё гораздо проще — достаточно объявить вспомогательную функцию внутри реализуемой, и первая получит доступ к переменным второй, так что нам не приходится прокидывать в неё дополнительный контекст через параметры:
class Symbol {
char[] id;
Symbol left;
Symbol right;
}
Symbol symbol_membersearch( Symbol[] table , char[] id ) {
Symbol sm;
void membersearchx( Symbol s ) {
while( s ) {
if( id == s.id ) {
if( sm ) error( "ambiguous member %sn" , id );
sm = s;
}
if( s.left ) {
membersearchx(s.left);
}
s = s.right;
}
}
for( int i = 0 ; i < table.length ; i++ ) {
membersearchx( table[i] );
}
return sm;
}
Динамические замыкания
Рассмотрим простой контейнерный тип. Чтобы быть реиспользуемым, ему необходимо уметь применять некоторый сторонний код к каждому элементу. В C это реализуется посредством передачи ссылки на функцию, которая и вызывается с каждым элементом в качестве параметра. В большинстве случаев дополнительно ей нужно передавать и некоторый контекст с состоянием. Для примера, передадим функцию вычисляющую максимальное значение чисел из списка:
void apply( void *p , int *array , int dim , void (*fp) ( void* , int ) ) {
for( int i = 0 ; i < dim ; i++ ) {
fp( p , array[i] );
}
}
struct Collection {
int array[10];
};
void comp_max( void *p , int i ) {
int *pmax = (int *) p;
if( i > *pmax ) {
*pmax = i;
}
}
void func( struct Collection *c ) {
int max = INT_MIN;
apply( &max , c->array , sizeof( c->array ) / sizeof( c->array[0] ) , comp_max );
}
В D вы можете передать так называемый делегат — функцию, привязанную к некоторому контексту. Когда вы передаёте куда-либо ссылку на функцию, которая зависит от контекста, в котором она объявлена, то на самом деле передаётся именно делегат.
class Collection {
int[10] array;
void apply( void delegate( int ) fp ) {
for( int i = 0 ; i < array.length ; i++ ) {
fp( array[i] );
}
}
}
void func( Collection c ) {
int max = int.min;
void comp_max( int i ) {
if( i > max ) max = i;
}
c.apply( &comp_max );
}
Или вариант по проще, с анонимным делегатом:
void func( Collection c ) {
int max = int.min;
c.apply( ( int i ) {
if( i > max ) max = i;
} );
}
Переменное число аргументов
Простой пример, как на C написать функцию, суммирующую все переданные ей аргументы, сколько бы их ни было:
#include <stdio.h>
#include <stdarg.h>
int sum( int dim , ... ) {
int i;
int s = 0;
va_list ap;
va_start( ap , dim );
for( i = 0 ; i < dim ; i++) {
s += va_arg( ap , int );
}
va_end( ap );
return s;
}
int main() {
int i;
i = sum(3, 8 , 7 , 6 );
printf( "sum = %dn" , i );
return 0;
}
Как видим, нам пришлось явно указать при вызове сколько параметров мы собираемся передать в функцию, что не просто избыточно с точки зрения программиста, но и является потенциальным источником трудноуловимых багов. Ну и куда же без традиционной проблемы — проверка передаваемых в функцию типов лежит целиком и полностью на совести программиста.
В D же есть специальная конструкция "..." позволяющая принять несколько параметров в качестве одного типизированного массива:
import std.stdio;
int sum( int[] values ... ) {
int s = 0;
foreach( int x ; values ) {
s += x;
}
return s;
}
int main() {
int i = sum( 8 , 7 , 6 );
writefln( "sum = %d", i );
return 0;
}
И наоборот, вы можете передать массив в функцию, которая принимает переменное число параметров:
int main() {
int[] ints = [ 8 , 7 , 6 ];
int i = sum( ints );
writefln( "sum = %d", i );
return 0;
}
Заключение
В этой статье мы рассмотрели преимущественно низкоуровневые возможности языка D, во многом являющиеся небольшим эволюционным шагом относительно языка C. В следующих статьях мы рассмотрим вопрос перехода с более мощного языка C++ и более простого Go. Оставайтесь на связи.
Автор: vintage