Каждый С-программист с опытом накапливает привычный багаж техник и идиом. Зачастую бывает сложно понять, как сделать то же самое в новом языке. Так вот, вашему вниманию предлагается коллекция распространенных паттернов на 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:\root\file.c char quoteString[] = "\"[^\\\\]*(\\\\.[^\\\\]*)*\""; // /"[^\\]*(\\.[^\\]*)*"/
В D в дополнение к обычным строкам с экранированием в стиле C, есть и так называемые «сырые строки», где экранирование не работает, и вы получаете ровно то, что ввели:
string file = r"c:\root\file.c"; // c:\root\file.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\ multiple\n\ lines\n"
В 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 %s\n" , 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 %s\n" , 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 = %d\n" , 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. Оставайтесь на связи.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
ссылка на оригинал статьи https://habrahabr.ru/post/276227/
Добавить комментарий