Каждый С-программист с опытом накапливает привычный багаж техник и идиом. Зачастую бывает сложно понять, как сделать то же самое в новом языке. Так вот, вашему вниманию предлагается коллекция распространенных паттернов на 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_normal


Таблица соответствия типов 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] );
}

Но, если есть возможность, лучше использовать итерирование по коллекции:

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 = 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 = realloc( s , ( lens + strlen( 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
}

Однако в C11 уже появилось ключевое слово alignas:

#include <stdalign.h>

struct data {
  char x;
  alignas(128) char cacheline[128]; // over-aligned array of char, not array of over-aligned chars
};


Анонимные структуры и объединения


C до версии 2011 года требует всем структурам давать имена, даже если они излишни:

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 (а также C11) поддерживает анонимные структуры, что позволяет выражать вложенные сущности более естественным образом, сохраняя плоский внешний интерфейс:

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 (до версии 1999 года) поля инициализируются в порядке их объявления, что не является проблемой для маленьких структур, но становится настоящей головной болью в случае структур больших, а также в случаях, когда необходимо изменить порядок следования и состав полей:

struct S { int a; int b; int d; int d; };
struct S x = { 5 , 3 , 2 , 10 };

В D (и в C99) вы тоже можете инициализировать поля по порядку, но лучше всё же явно указывать имена инициализируемых полей:

struct S { int a; int b; int c; int d; }
S x = { b : 3 , a : 5 , c : 2 , d : 10 };


Инициализация массивов


В 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 вы просто сравниваете значения, а компилятор обо всём позаботится (в 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\"\nmultiple\nlines\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. Оставайтесь на связи.
Ну что, на какой системный язык переходим?

Проголосовало 313 человек. Воздержалось 120 человек.

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

Комментарии (51)


  1. NeoCode
    31.01.2016 12:49
    +1

    У них у всех есть (возможно субъективные) недостатки.
    D мне нравится больше всех, у него наименее ломающий шаблоны (после С и С++) синтаксис и нет жесткого навязывания стиля программирования, как в Go и Rust. Но и D не лишен недостатков.


  1. BratSinot
    31.01.2016 13:01
    +4

    Если не ошибаюсь, то в C99 или C11 инициализировать массивы можно с помощью {0}.


    1. vintage
      31.01.2016 13:58

      Было бы славно дополнить статью поправками в свете свежих стандартов С от специалистов по этому языку.


      1. Yuuri
        31.01.2016 16:39
        +1

        Не специалист, но в C11 есть анонимные структуры.


      1. samo-delkin
        01.02.2016 07:53
        +1

        Для конкатенации строк используется strcat() и strncat(), причём всё это выглядит гораздо красивее.
        sizeof() — это не функция, а одноместная операция.
        В структурах можно инициализировать отдельные элементы через имена, начиная с C99.

        Хочешь посмотреть на красивый C — скачай исходники Git'а и посмотри.


        1. vintage
          01.02.2016 11:36
          +1

          strcat и strncat — просто копируют содержимое одного буфера в другой с учётом нулевых байтов, при этом в целевом буфере уже должна быть предварительно выделена память для такого копирования, иначе травма бедренной артерии гарантирована. В статье рассмотрен общий случай, когда у нас есть два буфера заранее неизвестной длины и нам нужно создать третий как соединение первых двух, и не ломая старые.

          Про структуры поправлю, спасибо.


          1. samo-delkin
            02.02.2016 03:00
            +1

            Я имел в виду вот этот фрагмент.

            Код
            // 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 ) );
            



            1. vintage
              02.02.2016 07:29
              -1

              Хоть я и не писал этого кода, позволю себе объяснить вам, почему он написан именно так:

              1. Это сделано для единообразия со всеми остальными типами. Без «бессмысленного sizeof» легко поменять char на какой-нибудь wchar_t и забыть домножить где-нибудь на 4.

              2. Без понятия, зачем там приведение. Возможно в старых версиях стандарта это было необходимо. Убрал его, чтобы оно вас не смущало. Код не сильно упростился.

              3. Да, размер строки может быть меньше размера буфера, так что правильнее использовать тут strlen без вариантов.

              4. memcpy гораздо быстрее, чем strcat, так что если длина первой строки заранее известна, то лучше использовать именно memcpy.

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


              1. samo-delkin
                02.02.2016 14:01
                +1

                Хоть я и не писал этого кода

                Да? Ну, значит, оттранслировал какую-то лажу. Тогда нельзя менять код, ведь нарушится идея того, кто это написал.
                Просто так получилось, что глупость автора выдаётся за несуразность C. Поэтому я тебе и предложил прочитать приличный код, где такой ерунды не встретишь.

                Без понятия, зачем там приведение.

                Такое приведение требуется в С++. Часто те, кто учил только C++, думают, что они автоматически узнают C таким образом, и тянут в него соответствующие конструкции.
                Особенно часто (практически всегда) распространена ошибка при определении функции main().

                memcpy гораздо быстрее, чем strcat, так что если длина первой строки заранее известна, то лучше использовать именно memcpy.

                Код должен быть ясным в первую очередь, а быстрым он должен быть во вторую.
                Если код неясный, то в нём легко закрадываются ошибки (ты пропустил такую ошибку, выделив на один байт больше).
                И что ты предлагаешь отказаться от strcat()? С какой стати?

                Было бы замечательно, чтобы вы вместо ты-ты-тыкания через каждое слово

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


                1. vintage
                  02.02.2016 17:42
                  -1

                  Нет, нужно не просто поменять код, но и сделать пулл-реквест.

                  Подозреваю тянут они это не просто так, а чтобы один и тот же код можно было использовать как из C, так и C++.

                  C — выбирают не за ясность кода, а за эффективность. Строки в C и так медленнее некуда. Незачем делать их ещё медленнее. А код с memcpy и c strcat отличаются лишь именами функций и необходимостью добавлять волшебную единицу во втором случае. Думаю изначально там и была реазизация с strcat, но потом решили соптимизировать и изменили на memcpy, но забыли стереть лишнюю единицу, которая в данном случае является magic number.

                  Ого, как много. Куда уж мне до вас. Выдыхайте чаще :-)


    1. Lertmind
      01.02.2016 05:11

      Такое
      int a[3] = { 0 }; /* { 0, 0, 0 } */
      в С99 точно есть, мне кажется с С89 можно было так.


      1. vintage
        01.02.2016 11:23

        А такое?

        int a[3] = { 1 }; /* { 1, 1, 1 } */
        


  1. lockywolf
    31.01.2016 13:37
    +6

    Странное сравнение. Если мне нужен «близкий к железу» код, то большинство плюсов D превращаются в минусы.

    Если же не нужен, то есть С++.

    Радость С в том, что он очень тривиально мапится на ассемблер. (Ну, на большинство ассемблеров для разумных архитектур.) А D с одной стороны вроде какбы тоже дольно низкоуровневый, но вот такие «упрощающие жизнь» штуки рвут представление о хорошем маппинге. Скажем, char.max — это что вообще такое? «Метод класса»? Макрос времени компиляции? Встроенная конструкция языка?

    В общем, лично мне (никому этого мнения не навязываю) несколько не хватает в С «общепринятых», в стиле буста, вспомогательных библиотек макросов/стандартных библиотек, упрощающих жизнь, но понятным образом разворачивающихся в сишный код.

    Писать «для сахара» отдельный язык мне кажется излишним.


    1. NeoCode
      31.01.2016 13:43

      Что такое «char.max» — это скорее вопрос правильной документации к языку. Такие вещи должны быть четко определены — как минимум compile-time или run-time.


      1. vintage
        31.01.2016 13:55
        +2

        Это свойства типа. Константы времени компиляции, проще говоря.


    1. vintage
      31.01.2016 14:10

      Ну вот глянул я краткое описание C11 и вижу, что часть из плюсов языка D перекочевали в C. Стал ли он от этого дальше от железа?

      Про C++ будет в следующей статье :-)

      D на самом деле разноуровневый и вы можете программировать на любом из уровней. От типов-сумм до встроенного ассемблера.


      1. lockywolf
        31.01.2016 14:38
        +1

        >>Стал ли он от этого дальше от железа?

        Трудно сказать. Я вот посмотрел на фичи по ссылке, и меня немного смущает только многопоточность, потому что лично я не понимаю, как её простым образом переводить в ассемблер. Другие же фичи по большей части скорее улучшают низкоуровневость, вроде макросов для alignment или atomic.

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


        1. creker
          31.01.2016 15:05

          меня немного смущает только многопоточность, потому что лично я не понимаю, как её простым образом переводить в ассемблер

          Очень просто, кто как напишет библиотеку, которая эту многопоточность реализует. Pthreads же как-то реализуют, теперь что-то подобное засунули в сам язык. С++ имеет намного больше возможностей, при этом все равно остается не менее близким к железу, чем С, поэтому его часто и выбирают, когда задача реализовать все максимально эффективно, но при этом не жертвовать мощью инструментов ради этого. И пришло время С наверстать упущенное в этом плане.


          1. Dima_Sharihin
            31.01.2016 15:54

            что-то подобное засунули в сам язык

            GCC требует линковки с pthreads, если используется многопоточность/асинхронщина в С++11.
            Так что это скорее «вы напишите это один раз, а компилятор разберется, как подружить ваш код с платформой», а не реальная фича компилятора


            1. creker
              31.01.2016 20:11

              Ну я это примерно и имел ввиду. Стандарт дает хедер <threads.h>, а потом код уже линкуется к платформозависимой библиотеке. Хотя и новое кодовое слово добавляется (_Atomic), так что и компилятор тут немного в деле будет.


      1. creker
        31.01.2016 15:16

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


        1. vintage
          01.02.2016 12:40

          Не всю, а только часть. Вот, например, реализация массивов, не зависящих от GC в противовес встроенным.


  1. zelyony
    31.01.2016 14:52
    +1

    нужна нормальная IDE для D — проекты, интеграция с системами контроля версий, визарды…
    и не надо разбегаться по разным «веткам»: одни ваяют D через LLVM, другие сами с усами — зачем им это? желание покопаться в ассемблере для разных архитектур? делай через LLVM — люди старались, оптимизировали, занимайся чистым D!


    1. redmanmale
      01.02.2016 12:44

      Это к JetBrains, у них неплохо получается.


    1. NeoCode
      01.02.2016 23:12

      Есть неплохой аддин для Студии


    1. vintage
      01.02.2016 23:21

      github.com/BBasile/Coedit

      • multi platform (Windows, Linux).
      • comprehensive project format and two advanced project editors (one for the options and configurations, another for the files).
      • DUB projects are supported too, with a an editor for the properties and an inspector to view and select sources or configurations.
      • all the D compilers are supported.
      • compile and run directly from the UI.
      • single click to compile and execute an unsaved module (aka a runnable module).
      • single click to compile and unittest a module.
      • advanced editor with D2 syntax highlighter, folds, regions, identifier markup, macros, sync-edit, etc.
      • list of the symbols in the module being edited.
      • static libraries manager that supports auto-registration from local DUB projects, from online DUB packages or from Coedit custom project format.
      • todo comments parser and dedicated widget.
      • user-defined tools powered by a string interpolation system.
      • full D Completion Daemon integration (completion, hints, call tips, jump to symbol declaration).
      • mini file browser, dfmt interface, search & replace, etc.
      • selection of 3rd party libraries that can be easily build and integrated to Coedit.


  1. Dicebot
    31.01.2016 15:02
    +3

    Этот код не скомпилируется:

    double.min
    


    Для числел с плавающей точкой требуется использовать свойство .min_normal — это сделано во избежание недопониманий о том, что это не является минимумом в том же смысле, что и у целых чисел.


    1. vintage
      31.01.2016 19:57

      Поправил, спасибо. Эта статься довольно старая, так что этот момент не нашёл в ней своё отражение.


  1. AoD314
    31.01.2016 21:42
    +3

    Спасибо за статью. Мне кажется, что было бы интересно сюда же добавить сравнение с Rust.


    1. vintage
      31.01.2016 22:21
      +4

      И до него очередь дойдёт :-)


      1. guai
        01.02.2016 20:34
        -1

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


  1. mwizard
    31.01.2016 23:21

    Если речь идет об отладке, то для превращения enum в строку можно просто использовать std.conv.to:

    assert(std.conv.to!string(Colors.red) == "red")

    Или, используя неявные функции,
    import std.conv;
    
    assert(Colors.red.to!string == "red");


    1. vintage
      31.01.2016 23:28

      В статье речь про отображение энума на произвольные строки, а не просто на одноимённые. А так, да, проще так:

      assert( Colors.red.to!string == "red" );
      


  1. Daniro_San
    01.02.2016 06:40

    А в C++ что мешает вынести весь сахар в одну либу и ей пользоваться — boost тому пример. Или написать свою бибилиотеку с сахаром с нуля с блекджеком и… ну вы поняли. Зачем мне тогда нужен D?


    1. beduin01
      01.02.2016 11:21
      +3

      Тем что подобная библиотека будет где-то сбоку от языка и до возможностей D дотягивать все равно не будет.


  1. gshep
    01.02.2016 09:14

    отличная шпаргалка!


  1. gnomeby
    01.02.2016 16:49

    А вы не подскажете, что вообще на D написано полезного на сегодняший день?


    1. vintage
      01.02.2016 17:20

      Имеется ввиду из библиотек или какие проекты его используют?


      1. gnomeby
        01.02.2016 17:24

        И так и так


        1. vintage
          01.02.2016 18:43

          1. gnomeby
            01.02.2016 19:24

            Однако для языка, которому столько лет, не так много знаковых проектов.


            1. vintage
              01.02.2016 19:35

              Для языка, в пиар которого не вкладывалась ни одна крупная корпорация — довольно много ;-)


              1. gnomeby
                01.02.2016 19:46

                Это голословное заявление. Давайте факты.


                1. vintage
                  01.02.2016 23:14

                  Интересно, какого рода вы ожидаете от меня фактов?


  1. Gorthauer87
    01.02.2016 20:03
    +2

    Мне кажется, что для D поезд уже ушел. Он бы круто выстрелил в тот момент, когда не было ещё C++11, а сейчас он уже не выглядит особо инновационным по сравнению с Swift, Rust, Scala, но при этом он заметно сложнее Go. Тем более, что на первых двух вполне можно системный софт делать.

    Идея пилить D2 — была фатальной ошибкой.


    1. creker
      01.02.2016 21:21
      -3

      Я все таки уверен, что D просто не хватило крепкого плеча корпорации. Go — тут тебе и гугл, и феномен докера, и хейт вокруг дизайна. Swift — вообще без вариантов. Rust — таки тащат, как могут, хотя язык по сложности превосходит все вместе взятые. А D — видеть этот пост про 5к строк от фейсбука — с одной стороны это достижение, а с другой это выглядит до того печально, что это похоже одно из наибольших достижений языка. А корпорация возможно бы решила другую проблему — ответить на вопрос, зачем же D нужен. Все остальные нашли свою нишу, а D все эти годы так и болтается непонятно где.


      1. creker
        04.02.2016 23:09

        Интересно знать, чего же я такого плохого написал, что аж три минуса получил? D не станет от этого живее и не получит поддержки громкого имени, которое ему жизненно необходимо.


    1. beduin01
      02.02.2016 09:23
      -4

      И какие же инновации есть у Swift, Rust, Scala, Go? Go вообще какой-то примитивный. Писать на нем после D откровенно неудобно. Rust хорошо подходит лишь для низкоуровневых задач, Scala — про нее слышу все реже и реже, как и про решения на ней. Инновации в С++11 могут радовать только тех кто на других языках не писал, в которых все это уже есть лет 15 как. При сколько там современный стандарт С++ занимает 1000? 1500 страниц? Вы уверены что все программисты смогут его прочитать, понять и правильно использовать?

      Пожалуй сейчас из интересных языков только Swift остается.


      1. creker
        02.02.2016 11:34

        Вот как раз swift ничего интересного и не содержит, в отличие от всяких Go и Rust. Первый — из-за рантайма и подхода к ООП. Второй из-за управления памятью. Swift просто пачка всем известных идей со странным синтаксисом не как у всех. Про Scala не знаю, но его преимущество кажется в совместимости с JVM. Примерно как в F# прямо.


        1. Gorthauer87
          02.02.2016 11:48

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


    1. guai
      02.02.2016 11:39

      Что угодно заметно сложнее Go :)