Мой интерес к дизайну языков программирования приводит меня иногда к интересным, но почти неизвестным в широких кругах проектам. Один из таких проектов - язык C∀ (CForAll), разрабатываемый в University of Waterloo. C∀ является расширением ISO C и обеспечивает обратную совместимость с C. Помимо исправления некоторых недостатков Си (которые можно исправить без нарушения обратной совместимости), в C∀ есть некоторые весьма интересные и оригинальные фичи: некоторые расширения классических управляющих операторов, альтернативный синтаксис объявления квалификаторов, кортежи и множественные операции, оригинальное расширение ссылок, полиморфизм, сопрограммы и т.д.
Данная статья представляет собой краткий конспект официального руководства по языку, с некоторыми моими комментариями.
Лексика
Первое мелкое, но важное улучшение - подчеркивания в числовых литералах. При этом несколько подчеркиваний подряд ставить нельзя.
2_147_483_648; // decimal constant
56_ul; // decimal unsigned long constant
0_377; // octal constant
0x_ff_ff; // hexadecimal constant
0x_ef3d_aa5c; // hexadecimal constant
3.141_592_654; // floating constant
10_e_+1_00; // floating constant
0x_ff_ff_p_3; // hexadecimal floating
0x_1.ffff_ffff_p_128 l; // hexadecimal floating long constant
L_"\x_ff_ee "; // wide character constant
Если требуется объявить идентификатор, совпадающий с ключевым словом, то используется вот такой синтаксис:
int ``otype = 3; // make keyword an identifier
double ``forall = 3.5;
Выражения и управляющие операторы
Оператор возведения в степень
Во многих языках пытаются его ввести, и везде придумывают разные операторые символы (^, ** и т.п.). На этот раз обратный слэш. Приоритет, как и полагается, выше чем у умножения.
x\y; // pow(x,y)
x\=y; // x=pow(x,y)
Улучшенный оператор множественного выбора
По недостаткам "сишного" оператора switch
проходятся все кому не лень. Всем известная особенность - в большинстве случаев нам не нужно выполнение всех последующих case-блоков после того, как был выполнен необходимый нам блок. Для предотвращения выполнения последующих блоков используется break
, однако программисты иногда забывают его поставить. Во всех современных языках уже перешли на match
, лишенный этого недостатка. Кроме того, в сишном switch
можно вставлять недостижимый код перед первым case
, и накладывать на switch другие блоки кода (Duff’s device). Все это запрещено в новом операторе choose
. В нем break
не требуется, а если нужно явно перейти к следующему блоку - предлагается использовать оператор fallthru
(или fallthrough
- первый раз вижу, чтобы два ключевых слова обозначали одно и то же).
Внутри case можно объявлять несколько паттернов через запятую; применять диапазоны; и несколько диапазонов через запятую.
switch ( i ) {
case 1, 3, 5: // ...
case 10 ∼ 15: // ...
case 1 ∼ 5, 12 ∼ 21, 35 ∼ 42: // ...
}
Диапазоны
Кстати, вот синтаксис диапазонов
1 ∼ 5 // полуоткрытый диапазон [M,N) : 1,2,3,4
1 ∼ =5 // закрытый диапазон [M,N] : 1,2,3,4,5
1 -∼ 5 // полуоткрытый диапазон [N,M) : 4,3,2,1
1 -∼=5 // закрытый диапазон [N,M] : 5,4,3,2,1
1 ∼ @ // диапазон до бесконечности
1 ∼ @ ∼ 2 // диапазон с указанием шага: 1,3,5,7,...
Диапазоны используются не только в case, но и в циклах. По сути эти диапазоны - частный случай List Comprehension ("списковых включений" или "генераторов списков", т.е. неких языковых конструкций, которые изнутри - вычисляемые выражения, а снаружи их можно применять вместо списков значений).
Улучшенные циклы
Некоторые маленькие улучшения в циклах
Цикл for по количеству элементов и по диапазонам
for ( i; 5 ) // typeof(5) i; 5 is comparison value
for ( i; 1.5 ∼ 5.5 ∼ 0.5 ) // typeof(1.5) i; 1.5 is start value
for ( 5 ) // for ( typeof(5) i; i < 5; i += 1 )
Циклы без аргументов - бесконечные циклы
(напомню, в С/С++ можно было написать for(;;)
, а для while все равно указывать аргумент true
)
while ( / * empty * / ) // while ( true )
for ( / * empty * / ) // for ( ; true; )
do ... while ( / * empty * / ) // do ... while ( true )
Метки для break и contunue
Как в Java - операторы могут передавать управление на метки. Такие переходы не могут образовывать цикл, и не могут вести внутрь управляющих структур; допускаются только выходы наружу из вложенных блоков.
Блок else для циклов
Как в Python - управление передается в блок else
, если внутри цикла не сработал break (т.е. выход из цикла был естественный, по условию самого цикла).
Оператор with
Оператор создает новую область видимости и представляет поля своего аргумента как локальные переменные внутри своего блока. Позаимствован из языка Pascal, и обобощает возможность С++ неявно обращаться к полям указателя this
. Может применяться к функциям или как отдельный блок. Может раскрывать сразу несколько составных объектов. При этом могут возникнуть конфликты имен, которые разрешаются обычным способом - явным уточнением.
void f( S & this ) with ( this ) {
c; i; d; // this.c, this.i, this.d
}
struct Q { int i; int k; int m; } q, w;
struct R { int i; int j; double m; } r, w;
with ( r, q ) {
j + k; // unambiguous, r.j + q.k
m = 5.0; // unambiguous, q.m = 5.0
m = 1; // unambiguous, r.m = 1
int a = m; // unambiguous, a = r.i
double b = m; // unambiguous, b = q.m
int c = r.i + q.i; // disambiguate with qualification
(double)m; // disambiguate with cast
}
Обработка исключений
Про обработку исключений написано немного, но заявляется, что поддерживаются как обычные исключения с раскруткой стека, так и исключения с возобнолением. Добавлен и отсутствующий в C++ оператор finally.
// E - тип исключения
void f() {
// ...
throwResume E{}; ... // resumption
// ...
throw E{}; // termination
}
try {
f();
}
catch( E e ; boolean-predicate ) { // termination handler
// recover and continue
}
catchResume( E e ; boolean-predicate ) { // resumption handler
// repair and return
}
finally {
// always executed
}
Альтернативный синтаксис объявления квалификаторов
Синтаксис объявления переменных С/С++ славится своей сложностью - особенно когда дело касается типов и объектов со множеством квалификаторов, что-то вроде массива указателей на функции, принимающих массивы указателей и возвращающих указатели на массивы. Какие-то квалификаторы относятся к типу, какие-то - к объекту; одни пишутся перед именем переменной, другие - после. Это приводит к различным неоднозначностям, которые я возможно опишу в другой статье.
В современных языках для упрощения парсинга и борьбы с неоднозначностями обычно применяют форму с начальным ключевым словом (var
, let
и т.п.) и "типом справа" по отношению к объектам. В С∀ решили сохранить "тип слева", но разместили все квалификаторы перед типом. В результате синтаксис стал однозначным, чтение и запись - простыми и естественными. Вот прямо так: "указатель на массив из 10 указателей на целое": *[10]*int
. Да, теперь все квалификаторы относятся к типу, и исчезла возможность объявлять разные квалификаторы для разных переменных в одной строке; но и в других языках (в которых "тип справа") этой возможности нет. Фактически, это исключительная особенность С/С++, не так уж и часто используемая.
Еще примеры. Читается очень просто, не так ли?
const * [ 5 ] const int y; // const pointer to array of 5 const integers
const * const int x; // const pointer to const integer
static * const int y; // internally visible pointer to constant int
Что интересно, в С∀ в целях обратной совместимости оставили и старый синтаксис деклараций. Понятно что это криво, но здесь интересна сама идея того, как можно упростить сложные декларации, а не конкретная реализация.
Указатели и ссылки
В большинстве языков указатели требуют явного разыменования. В некоторых языках, таких как Алгол68, осуществляется неявное разыменование указателей в выражениях.
p2 = p1 + x; // compiler infers *p2 = *p1 + x;
Неявное разыменование дает более компактный код. Оно подходит, если работа со значениями значительно более востребована, чем работа с адресами. Для сравнения:
* p2 = (( * p1 + * p2) * ( ** p3 - * p1)) / ( ** p3 - 15);
p2 = ((p1 + p2) * (p3 - p1)) / (p3 - 15);
Для поддержки неявного разыменования в С∀ введены ссылки. Они похожи на ссылки C++, но отличаются в некоторых аспектах.
int x, y, & r1, & r2, && r3;
&r1 = &x; // r1 points to x
&r2 = &r1; // r2 points to x
&r1 = &y; // r1 points to y
&&r3 = &&r2; // r3 points to r2
r2 = ((r1 + r2) * (r3 - r1)) / (r3 - 15); // implicit dereferencing
Инициализация ссылок осуществляется конструкцией &r = &v
. Важно, что инициализация синтаксически отличается от присваивания r = v
. В С++ для обоих действий используется обычное присваивание, хотя по смыслу действия были разные - в первом случае брался адрес переменной и сохранялся в ссылке (неявном указателе), во втором - значение переменной записывается по адресу, хранимому в ссылке.
Ссылки могут быть двойными, тройными и т.д. (конструкция && r3
). Это полностью аналогично указателям: ссылка на ссылку означает, что переменная - неявный указатель, хранящий адрес другого неявного указателя. Указатели и ссылки взаимозаменяемы, поскольку оба содержат адреса. Отличается только синтаксис
int x, *p1 = &x, **p2 = &p1, ***p3 = &p2, &r1 = x, &&r2 = r1, &&&r3 = r2;
*** p3 = 3; // change x
r3 = 3; // change x, ***r3
** p3 = ...; // change p1
&r3 = ...; // change r1, (&*)**r3, 1 cancellation
* p3 = ...; // change p2
&&r3 = ...; // change r2, (&(&*)*)*r3, 2 cancellations
&&&r3 = p3; // change r3 to p3, (&(&(&*)*)*)r3, 3 cancellations
Ссылки могут иметь квалификаторы. Сама ссылка & является квалификатором и подчиняется тем же правилам, что и квалификатор указателя и массива.
В противоположность С∀ , ссылки С++ являются неизменяемыми однократно инициализируемыми. Также С++ не поддерживает массивы ссылок.
Смысл инициализации ссылок отличается от присваивания, поскольку она происходит для пустого (неинициализированного) объекта (т.е. до инициализации ссылка никуда не указывает). Поэтому имеет смысл только семантика адреса, т.е. само инициализирующее значение должно быть адресом. Маловероятно, что присвоение значения x указателю осмысленно. Следовательно, при инициализации ссылки требуется адрес, и излишне требовать явного взятия адреса объекта, которым инициализируют ссылку.
int * p = &x; // assign address of x 9
int * p = x; // assign value of x 10
int & r = x; // must have address of x
По той же причине не требуется оператор взятия адреса при передаче по ссылке в функцию. Также если возвращаемое значение - ссылка, то при присваивании результата оператор ссылки не требуется.
int & f( int & r ); // reference parameter and return
z = f( x ) + f( y ); // reference operator added, temporaries needed for call results
Можно получать адреса и ссылки литералов и выражений. Причем эти адреса и ссылки могут быть не только константными, но и изменяемыми. Компилятор сам создает необходимые временные объекты и использует их адреса.
void f( int & r );
void g( int * p );
f( 3 );
g( &3 ); // compiler implicit generates temporaries
f( x + y );
g( &(x + y) ); // compiler implicit generates temporaries
Подводя итоги: интуитивно понятно, что авторы сделали полностью симметричную систему ссылок и указателей, с той лишь разницей, что у указателя - явное разыменование при доступе к значению, а у ссылки соответствующая lvalue-операция "взятия адреса" при изменении самой ссылки. Выглядит, по правде говоря, мозгодробительно. Но зато ссылка стала first-class объектом (пусть и специфическим). Стоило ли делать вот именно так? Насколько часто востребована операция неявного разыменования, если все равно используются различные "умные указатели"?
Перечисления
Кроме обычных, в С∀ доступны нецелочисленные (по сути - объектные) перечисления. Базовый тип указывается в круглых скобках после ключевого слова enum.
enum( double ) Math { PI_2 = 1.570796, PI = 3.141597, E = 2.718282 }
enum( char * ) Name { Fred = "Fred" , Mary = "Mary" , Jane = "Jane" };
enum( [int, int] ) { T1 = [ 1, 2 ], T2 = [3, 4] }; // tuples
enum( S ) s { A = { 5, 6 }, B = { 7, 8 } }; // struct S { int i, j; };
Перечисления одного типа могут "наследоваться" - точнее, "включаться" одно в другое.
enum( char * ) Name2 { inline Name, Jack = "Jack" , Jill = "Jill" };
enum Name3 { inline Name2, Sue = "Sue" , Tom = "Tom" };
Разумеется, имена в базовом перечислении и в перечислении-наследнике должны быть уникальными.
Структуры
Безымянные поля структур
Можно вставлять в структуры любое количество безымянных полей. Это может быть полезно, например, для формирования каких-то структур с зарезервированными полями.
struct {
int f1; // named field
int f2 : 4; // named field with bit field size
int : 3; // unnamed field for basic type with bit field size
int ; // disallowed, unnamed field
int * ; // disallowed, unnamed field
int ( * )( int ); // disallowed, unnamed field
};
Вложенные структуры
В Си можно описывать одну структуру внутри другой, но она все равно будет располагаться в глобальном пространстве имен. В С∀ исправили эту странность - имя объемлющей структуры стало пространством имен для вложенной. В отличие от С++, в С∀ для доступа ко вложенным сущностям используется точка, а не ::
Встраивание
Одну структуру можно встроить в другую так, как это реализовано в Go (и даже лучше - используется ключевое слово inline
). Это очень простая и в то же время мощная концепция, прямо готовая для proposal'а в очередной стандарт С и/или С++... Удивительно - почему ее сразу не сделали в Си?
struct Point { double x, y, x; };
struct ColoredPoint {
inline Point; // anonymous member (no identifier)
int Color;
};
ColoredPoint cp;
cp.x = 10.3; // x from Point is accessed directly
cp.Color = 0x33aaff;
Кортежи
Множественный возврат из функций
В Си и в большинстве языков программирования функция возвращает только одно значение; но иногда нужно больше. Чтобы вернуть больше, применяют агрегацию (возврат структуры с несколькими полями) или возврат через аргументы по указателю/ссылке. В С∀ сделана попытка реализации непосредственного возврата из функции нескольких значений. Для этого используются квадратные скобки:
[ char, int, double ] f()
{
return [ 'c', 123, 3.14 ];
}
Использование нескольких возвращаемых значений:
int quot, rem; 37
[ quot, rem ] = div( 13, 5 );
Каждый элемент кортежа может быть чем угодно, в т.ч. и другим (вложенным) кортежем.
Передача сразу нескольких значений в функцию
При передаче в функцию кортеж разворачивается сразу в несколько значений.
printf( "%d %d\n" , qr ); // print quotient/remainder
Объявление объектов кортежей
[ double, int ] di;
[ double, int ] * pdi;
[ double, int ] adi[10];
Можно объявить и сразу инициализировать, например возвратом из функции
[int, int] qr = div( 13, 5 );
Доступ к отдельным элементам
Похоже на реализацию в Swift: используются константные номера элементов и оператор "точка" (а для указателей и "стрелка").
[int, double] x;
[char * , int] f();
[int, double] * p;
int y = x.0; // access int component of x
y = div(20,7).1; // access int component of functions return
p->0 = 5; // access int component of tuple pointed-to by p 39
y = [ f(), x ].1.0; // access first component of second component of tuple expression
Флаттернизация и структуризация
Кортежи не имеют жесткой структуры и могут при необходимости структурироваться и деструктурироваться. Функция, принимающая несколько аргументов, может принять соответствующий кортеж, и наоборот - функция, принимающая кортеж, может принять несколько аргументов. Т.е. кортежи могут неявно раскрываться и наоборот, неявно формироваться в соответствии с контекстом. Например, есть функция и ее вызов:
int f(int, [double, int]);
f([5, 10.2], 4);
Сначала список аргументов раскрывается в 5, 10.2, 4
, затем структурируется в 5, [10.2, 4]
.
Присваивание
Предусмотрено "массовое" и "множественное" присваивание
[y, x] = 3.14; // mass assignment
[x, y] = [y, x]; // multiple assignment
При множественном присваивании размеры кортежей должны совпадать. Оба вида присваивания распараллеливаются.
Множественный доступ к полям структур
Еще одна красивая фича:
struct S { char x; int y; double z; } s;
s.[x, y, z] = [ 3, 3.2, ' x ' ];
f( s.[ y, z ] );
Функции
Именованные аргументы
Функцию вида void foo( int x, int y, int z ) {...}
можно вызывать с именованными аргументами:
foo( z : 3, x : 4, y : 7 );
Аргументы по умолчанию
Фича как в С++ и множестве других языков. Однако, можно пропускать аргументы не только в конце списка, но и в любом другом месте, просто поставив нужное количество запятых.
void p( int x = 1, int y = 2, int z = 3 ) {...}
p(); // rewrite ⇒ p( 1, 2, 3 )
p( 4, 4 ); // rewrite ⇒ p( 4, 4, 3 )
p( , 4, 4 ); // rewrite ⇒ p( 1, 4, 4 ) -- можно пропускать в начале!
p( 4, , 4 ); // rewrite ⇒ p( 4, 2, 4 ) -- и в середине
p( , , 4 ); // rewrite ⇒ p( 1, 2, 4 )
p( , , ); // rewrite ⇒ p( 1, 2, 3 )
Вложенные функции
В С∀ функции можно объявлять внутри других функций. Такие вложенные функции не являются first-class объектами, т.е. это не полноценные "лямбды" с "замыканиями", их нельзя возвратить из функции в качестве результата и они не захватывают объекты из объемлющего контекста. Но тем не менее вложенные функции могут обращаться к локальным переменным объемлющих. Такая реализация ближе всего к вложенным процедурам Pascal.
int foo() {
int i = 7;
void bar( int j ) {
i += j;
}
bar(10);
return i;
}
Постфиксные функции
Альтернативный синтаксис вызова, при котором аргумент указывается перед именем функции. Обычно используется для преобразования базовых литералов в пользовательские литералы, где ?`
обозначает имя постфиксной функции, а
`
обозначает вызов постфиксной функции. Например, следующий код преобразует литералы, представляющие физические величины, в другие единицы измерения.
double ?`ft(double f) { return f / 3.28084; }
printf("100 feet == %f meters\n", 100`ft);
Постфиксные функции могут быть и с несколькими аргументами, в этом случае постфикс применяется к кортежу.
Перегрузка
Перегрузка функций
Язык поддерживает перегрузку функций. Что интересно, заявлена поддержка перегрузки по возвращаемому значению.
int f();
double f();
f(); // ambiguous
(int)f(); // choose "int f()"
Перегрузка операторов
Также можно перегружать операторы. Используется специальный синтаксис со знаками вопроса, обозначающими операнды. Оператор {} - это "конструктор", используемый для инициализации объектов.
type Complex = struct {
double real;
double imag;
}
void ?{}(Complex &c, double real = 0.0, double imag = 0.0) {
c.real = real;
c.imag = imag;
}
Complex ?+?(Complex lhs, Complex rhs) {
Complex sum;
sum.real = lhs.real + rhs.real;
sum.imag = lhs.imag + rhs.imag;
return sum;
}
Перегрузка переменных
Да, такое тоже возможно. Несколько переменных разных типов с одним именем:
int pi = 3;
float pi = 3.14;
char pi = .p.;
Полиморфизм
Одна из ключевых особенностей С∀ - это перегружаемые параметрически-полиморфные функции, обобщенные с помощью оператора forall (язык назван именно по этому ключевому слову). Внешне - что-то вроде шаблонной функции:
forall( otype T ) T identity( T val ) { return val; }
int forty two = identity( 42 );
Эту функцию можно применить к любому полному объектному типу (otype
). C∀ передает размер и выравнивание типа, представленного параметром otype
, а также оператор присваивания, конструктор, конструктор копирования и деструктор. Если эта дополнительная информация не нужна, например, для указателя, параметр типа может быть объявлен как тип данных (dtype
). Еще бывает ftype
(функциональный тип) и ttype
(тип-кортеж).
Поддерживается что-то вроде концептов. Например, это работает для всех типов, поддерживающих сложение:
forall(otype T | { T ?+?(T,T); }) T twice (T x) { return x+x; }
int val = twice(twice(3));
Уcловия концептов можно группировать в трейты
trait sumable( otype T ) {
void ?{}( T &, zero_t ); // constructor from 0 literal
T ?+?( T, T ); // assortment of additions
T ?+=?( T &, T );
T ++?( T & );
T ?++( T & );
};
и затем использовать
forall( otype T | sumable( T ) ) // polymorphic, use trait
T sum( T a[ ], size_t size ) {
T total = 0; // instantiate T from 0 by calling its constructor
for ( i; size ) total += a[i]; // select appropriate +
return total;
}
int sa[ 5 ];
int i = sum( sa, 5 ); // use int 0 and +=
Паралеллизм
Заявлена поддержка стековых и бесстековых сопрограмм, а также потоков. Интересное решение - имя main с аргументом соответствующего типа используется как имя главной функции (точки входа) сопрограммы или потока.
Генераторы
Генераторы - бесстековые сопрограммы (т.е. они используют стек вызывающей стороны). Ключевое слово suspend используется для приостановки корутины и возврата управления в вызывающий контекст, и ключевое слово resume - для возобновления выполнения корутины с того места, где она вернула управление.
generator Fibonacci {
int fn; // used for communication
};
void main( Fibonacci & fib ) { // called on first resume
int fn1, fn2; // retained between resumes
fib.fn = 0;
fn1 = fib.fn; // 1st case
suspend; // restart last resume
fib.fn = 1;
fn2 = fn1;
fn1 = fib.fn; // 2nd case
suspend; // restart last resume
for () {
fn = fn1 + fn2; fn2 = fn1; fn1 = fn; // general case
suspend; // restart last resume
}
}
int next( Fibonacci & fib ) {
resume( fib ); // restart last suspend
return fib.fn;
}
Корутины
Стековые сопрограммы также поддерживаются
#include <fstream.hfa>
#include <coroutine.hfa>
// match left/right parenthesis: ((())) match, (() mismatch
enum Status { Cont, Match, Mismatch };
coroutine CntParens {
char ch; // used for communication
Status status;
};
void main( CntParens & cpns ) with( cpns ) { // coroutine main
unsigned int cnt = 0;
for ( ; ch == '('; cnt += 1 ) suspend; // count left parenthesis
for ( ; ch == ')' && cnt > 1; cnt -= 1 ) suspend; // count right parenthesis
status = ch == ')' ? Match : Mismatch;
}
void ?{}( CntParens & cpns ) with( cpns ) { status = Cont; }
Status next( CntParens & cpns, char c ) with( cpns ) { // coroutine interface
ch = c;
resume( cpns );
return status;
}
int main() {
CntParens cpns;
char ch;
for () { // read until end of file
sin | ch; // read one character
if ( eof( sin ) ) { sout | "Mismatch"; break; } // eof ?
Status ret = next( cpns, ch ); // push character for checking
if ( ret == Match ) { sout | "Match"; break; }
if ( ret == Mismatch ) { sout | "Mismatch"; break; }
}
}
Мониторы
Конструкции, аналогичные структурам, но с неявной (генерируемой компилятором) защитой полей от одновременного доступа из разных потоков.
monitor Account {
const unsigned long number;
float balance;
};
Потоки
#include <fstream.hfa>
#include <thread.hfa>
thread T {
int id;
};
void ?{}( T & t ) { t.id = 0; }
void ?{}( T & t, int id ) { t.id = id; }
void main( T & t ) with( t ) { // thread starts here
sout | id;
}
int main() {
enum { NumThreads = 5 };
T t[ NumThreads ]; // create/start threads
T * tp[ NumThreads ];
for ( i; NumThreads ) {
tp[i] = new( i + 1 ); // create/start threads
}
for ( i; NumThreads ) {
delete( tp[i] ); // wait for thread to terminate
}
}
Что дальше
Это очень краткий обзор, упускающий многие детали и не рассматривающий некоторые вопросы. Страница проекта. На сайте есть ссылка на гитхаб. Также в сети есть несколько публикаций, посвященных разным аспектам языка.
Все это можно проверить (что я и делал в некоторых непонятных случаях): исходники компилятора и инструкции для сборки доступны на гитхабе. Единственная особенность - сборка зачем-то требует рута, нужна запись в /usr/local/bin и возможно еще куда-то. Наверное это можно исправить, но я не специалист в сборочных скриптах make, да и под виртуалкой как-то без разницы.
Лично мне всегда интересно посмотреть на альтернативные языки программирования, альтернативные и расширенные реализации привычных вещей. То, что язык не слишком сильно отличается от самых распространенных языков - для меня плюс, не нужно ломать мозги для адаптации к новому синтаксису, а можно спокойно изучать новые идеи в привычном окружении. Надеюсь что вам тоже понравилось.
Комментарии (71)
IkaR49
05.11.2021 00:20+2const * [ 5 ] const int y;
А мне нравится. Всё остальное "ну так", а это прям нравится.
northzen
05.11.2021 01:04+8Мне кажется, сейчас новые языки взлетают только если они способны предложить или большую экосистему, включающую старые библиотеки, или если за языком стоит корпорация, продвигающая язык в своих продуктов, а поэтому пилящяя для него ту же самую экосистему, обвязки, компиляторы, чекеры и т.д.
Либо язык действительно решает КРАТНО лучше задачи, чем его существующие альтернативы.
Для плюсов альтернатива нужна не только в языке, но скорее в нормальном менеджере пакетов, например, и прозрачной системе сборки.
Когда новый язык везде делает чуть лучше, чем в старом, при этом чуть лучше почему-то сделано специально по-другому, чем в иных языках (ну зачем обратная косая черта для возведения в степень?)
Пока не очень понятна ниша. Язык же теперь больше чем язык, это еще и экосистема.khim
05.11.2021 04:42+8Для плюсов альтернатива нужна не только в языке, но скорее в нормальном менеджере пакетов, например, и прозрачной системе сборки.
Самая большая проблема плюсов — то, что в него добавили такую кучу неопределённых поведений, что писать программы стало практически невозможно (и собираются добавить ещё).
Совершенно идиотское сочетание двух правил:
Любая синтаксически корректоная программа, написанная пусть даже и 20 лет назад обязана компилироваться.
Если при этом, во время исполненения, программа приводит к неопределённому поведению (в том числе не существовавшему на момент написания оной программы!) — то программу можно ломать.
Результат: как бы ты ни старался, но рано или поздно появится компилятор, в соответствиями с глубокими идеями которого, твоя программа не является корректной и потому она не работает, а ты сидишь в отладчике.
Нужен язык, в котором не нужно писать программы так, как будто ты ходишь по минному полю.
Практически таковых есть два: Swift (у Apple) и Rust (для всех остальных).
Как с этим у С∀ — но, подозреваю, что ещё хуже, чем у C и C++.
alliumnsk
05.11.2021 11:56+1UB появляется не оттого, что много добавлено, а от желания использовать язык на самого рода существенно разных архитектурах без издержек. Раньше были архитектуры с 9-битными байтами, и не с комплементарным представлением отрицательных чисел. Новые языки, как правило пишутся исходя из того все процессоры более-менее одинаковые, а если надо на сильно отличающийся процессор -- эмулируйте как хотите.
khim
05.11.2021 15:07+5UB появляется не оттого, что много добавлено, а от желания использовать язык на самого рода существенно разных архитектурах без издержек.
О том как и почему появилось это понятие я сам когда давно писал.
Когда неопределённое поведение появилось другой альтернативы этому всему, в общем, не было.
Однако долгое время между разработчиками компиляторов и пользователями был негласный уговор: вы можете придумывать себе разные правила, но реальные программы не должны ломаться.
Лет десять назад это правило начало, довольно-таки грубо, нарушаться: компиляторы начали целенаправленно ломать программы, которые, формально приводили к неопределённому поведению, однако, при этом, работали годами и десятилетиями.
Более того: как сейчас выясняется ещё в 2004м году разработчики компиляторов выбили себе карт-бланш на UB, которые они до сих пор не могут описать!
Вдумайтесь в уровень этого идиотизма: правила, нарушение которых позволяет вашему компилятору сломать, к чёртовой матери, вашу программу до сих пор не описаны в стандарте потому что их никак не могут разработать (ибо там противоречия воникают), ни в одном стандарте их нет (одна из последних попыток их добавить), но если вы их нарушаете — то получаете проблемы. Класс, да? Как таким языком вообще можно пользоваться? Это ж минное поле получается!
Новые языки, как правило пишутся исходя из того все процессоры более-менее одинаковые, а если надо на сильно отличающийся процессор -- эмулируйте как хотите.
Фишка же в том, что ровно это и предполагалось делать с C. Это было даже явно прописано в соответствующем документе: Undefined behavior gives the implementor license not to catch certain program errors that are difficult to diagnose. It also identifies areas of possible conforming language extension: the implementor may augment the language by providing a definition of the officially undefined behavior.
Выделение моё, но и первая часть тоже важна: неопределённое поведение, изначально, рассматривалось как что-то, что хорошие компиляторы могут отлавливать — но не обязаны. И со временем планировалось количество этих чудес уменьшать и делать язык более предсказуемым.
Хорошее решение во времена, когда рабочая станция имела более слабый процессор чем тостер или зарядка для телефона сегодня.
К сожалению на практике получилось всё ровно наоборот: чем дальше в лес, тем
толще партизанысложнее писать программы, которые компилялор не превратит в труху.Можно долго обсуждать причины этого явления (вот, например, хорошая статья), но в сухом остатке: жалкие попытки вернуться к тому пути, который намечался в прошлом веке окончились ничем. К сожалению.
Раньше были архитектуры с 9-битными байтами, и не с комплементарным представлением отрицательных чисел.
Дык фишка вот в чём: сейчас таких архитектур нету и современные версии C++ их не поддерживают, однако компиляторам по прежнему разрешено ломать программы, которые пользуются тем, что в современных процессорах комплемендарное представление отрицательных чисел не используется!
tyomitch
05.11.2021 19:31Неопределённое поведение — это всё то, чьё поведение не было определено.
Поэтому пожелания «описать все случаи неопределённого поведения» абсурдны — примерно как попытка описать число через его неописуемость.
Другими словами: это не какие-то особые запрещённые случаи, прихотливо разбросанные по пространству допустимых программ. Наоборот: стандарт определяет поведение довольно узкого подмножества всех синтаксически корректных программ. Всё то, что не определено — неопределённо.
И я не знаю ни одного случая, когда поведение, определённое более старым стандартом языка, в более новом становилось бы неопределённым. ПоэтомуЕсли при этом, во время исполненения, программа приводит к неопределённому поведению (в том числе не существовавшему на момент написания оной программы!) — то программу можно ломать.
--либо неуклюже сформулировано (поведение программы никогда не было определено, но поведение компиляторов с момента написания программы изменилось), либо фактически неверно.khim
05.11.2021 23:58+6Поэтому пожелания «описать все случаи неопределённого поведения» абсурдны — примерно как попытка описать число через его неописуемость.
Это что за идиотизм? Открываете любой стандарт C (не C++!), смотрите в приложение J. Там все случаи описаны.
В C++ они не сгруппированы в одном месте, а разбросаны по тексту, но тоже все описаны.
Неопределённое поведение не означает, что ситуация, в которой оно возникает не определено. Вот последствия — да, не определены. А действия, совершаемые программой для того, чтобы мы могли заявить “да, у нас-таки случилось UB” все описаны, в противном случае мы не могли бы вообще, в принципе, определить — приводит программа к UB или нет, определено её поведение или может быть любым.
Другими словами: это не какие-то особые запрещённые случаи, прихотливо разбросанные по пространству допустимых программ.
Это особо запрещённые случаи, прихотливо разбросанные по множеству допустимых действий. И то, что они не определены в стандарте не означает, что они совсем никак и никем не определены.
Пример: разименование нулевого указателя. В разных системах это может привести к разным последствиям (скажем в MS DOS вы обратитесь к адресу обработчика нулевого перерывания и можете его испортить, а в Linux или Windows x64 система отдиагностирует ошибку и вашу программу остановит… хотя ошибку можно перехватить и обработать).
Я не знаю вообще ни одной системы, где бы это поведение не было бы описано! Да, не в стандарте C или C++, а в стандарте POSIX или в даташите на SOC… но оно таки описано!
Но разработчикам копиляторов пофиг: раз стандарт C++ говорит, что это неопределённое поведение — значит у нас карт-бланш, будем крушить всё, до чего сможем дотянуться.
Или другая “типа оптимизация”, превращающая
i || !i
не вtrue
(что было бы понятно и логично), но в false. Это не так-то просто было сделать, закон исключённого третьего, вроде бы, должен бы подобные вещи отсекать. Но… напряглись! Смогли! Ура, товарищи, теперь программисты получили ещё один способ выстрелить себе в ногу!И я не знаю ни одного случая, когда поведение, определённое более старым стандартом языка, в более новом становилось бы неопределённым.
Врёте. Вот в том самом примере с realloc, вокруг которого столько копий ломали никто так и не придумал как можно найти UB, интерпретируя программу с точки зрения C89. Ибо “гениальная” интепретация “когда realloc вернул тот же указатель, что и до вызова realloc, то это всё равно другой объект” можно вычитать из C99 и последующих стандартов, но не из C89.
Да и то — их нужно невероятно “креативно” читать, чтобы в этой программу UB углядеть. Нормальному программисту никогда в жизни не придёт в голову идея, что вот такой вот код:
p = realloc(q, …)
; делаетq
, внезапно, указывающим на место в памяти один-после-конца массива (нулевого размера, видимо)! А только такая, “креативная”, интерпретация позволяет заявить, что в этой программе имеется UB.--либо неуклюже сформулировано (поведение программы никогда не было определено, но поведение компиляторов с момента написания программы изменилось), либо фактически неверно.
Ни то, ни другое, увы. Окройте же, наконец, пропозал, почитайте и ужаснитесь: там целая куча примеров того, как разные компиляторы ломают валидные программы — с предложением вот всё вот то безобразие, которое компиляторы вытворяют объявить валидным, а стандарт изменить, чтобы эти прораммы перестали быть валидными программами на C++.
Примерно как это уже произошло с
realloc
ом. А потому что в 2004м году было решение комитета по стандартизации, в стандарт ничего протащить не удалось (а ведь 17 лет прошло!), но компиляторы уже считают, что некоторые “плохие” (но не нарушающие стандарт, заметим!) программы можно ломать. И ломают. Красота?tyomitch
06.11.2021 00:57+1В примере с
q = realloc(p, …)
вы не имеете права использоватьp
после успешного вызова, т.е. когдаq != NULL
: ни разыменовывать, ни сравнивать. Цитата из C89: The pointer returned if the allocation succeeds is suitably aligned so that it may be assigned to a pointer to any type of object and then used to access such an object in the space allocated (until the space is explicitly freed or reallocated). <...> The value of a pointer that refers to freed space is indeterminate. Не вижу, о чём тут ломать копья: сама попытка определить, тот же указатель возвращён или другой — уже UB. Освобождённый указатель не «указывает на место в памяти один-после-конца массива», а равнозначен неинициализированному.demoth
06.11.2021 02:04+3Ну в процитированном абзаце, всё на самом деле упирается в то, как понять reallocated. Я вижу два способа: 1) фактическая реаллокация, т.е. память была перемещена на новое место, 2) тупо факт вызова функции realloc. И UB тут можно натянуть только при второй трактовке, что ну очень натянуто. А если учесть ещё, что в первом подчёркнутом предложении речь идёт именно о памяти (in the space allocated), а не о значении указателя, то первая трактовка кажется единственно верной.
khim
06.11.2021 02:42+3В примере с
q = realloc(p, …)
вы не имеете права использоватьp
после успешного вызова, т.е. когдаq != NULL
: ни разыменовывать, ни сравнивать.С какого такого перепугу?
Не вижу, о чём тут ломать копья: сама попытка определить, тот же указатель возвращён или другой — уже UB.
А вот фиг. Вы же сами привели цитату правильного места и даже выделили, просто не захотели в неё вчитаться. Вот про разименование указателия:
until the space is explicitly freed or reallocated
Использовать указатель нельзя в двух случаях: когда память освобождена и когда она реаллоцирована.
А вот и про испольование самого значения указателя:
The value of a pointer that refers to freed space is indeterminate
Смотреть на него — запрещено только в одном случае. Если память была освобождена. Про реаллокацию в этом месте — ни звука.
И кстати, то же самое даже в C18. Там это правило вообще в другом месте, но звучит оно так: The behavior is undefined in the following circumstances: … the value of a pointer that refers to space deallocated by a call to the
free
orrealloc
function is used.Обратите внимание: тут снова идет не речь об указателе на исчезнувший объект, а об указателе, указывающем на освобождённую память. А вот сравнить два указателя (чтобы понять, указывают один из них на освобождённую памяти или нет) нам разрешили явно: The
realloc
function returns a pointer to the new object (which may have the same value as a pointer to the old object), or a null pointer if the new object has not been allocated.Вот это вот самое замечание в скобочках явно разрешает нам эти указатели сравнивать. Если бы их сравнение приводило бы к UB, то замечание в скобках не имело бы смысла. Собственно оно и было добавлено, чтобы эти указатели можно было сравнивать, в C89 этого было не нужно (ещё раз читаем о том, когда указатели можно сравнивать, а когда разименовывать).
Освобождённый указатель не «указывает на место в памяти один-после-конца массива», а равнозначен неинициализированному.
Нет. Сравнивать нам эти два указателя разрешили явно, а это значит, что вступет в игру правила сравнения указателей:
Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function,both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.
Тут у нас появился очень-очень интересный момент, которого в C89 не было. Там правило было другое:
If two pointers to object or incomplete types compare equal. they both are null pointers. or both point to the same object. or both point one past the last element of the same array object.
Интригующий и очень-очень хитрый вариант, через который всю эту историю с
realloc
ломастеры, пишущие компиляторы, хотят протащить валидность этой горе оптимизации — он здесь в принципе отсуствует! Если уж два указателя равны, так они таки равны. И либо оба можно использовать, либо оба нельзя использовать.А в C99, да, поскольку обращаться к объекту, который был уничтожен нельзя (этого момента в C89 тоже нет, как и уничтожения и создания нового объекта в
realloc
), и у нас резрешён невозможный в C89 случай, когда два указателя равны, но один из них валиден, а другой невалиден, то можно попробовать сову на глобус натянуть и объяснить, что, поскольку объект-таки удалён, а на его месте теперь новый, то даже если два указателя равны, то одним можно пользоваться, а другим нельзя. Ну, типа объект схлопнулся, память мы не освободили, но объект уничтожили, потому это — теперь вот такой вот one past the end of one array object указатель.Мне эта интерпретация кажется несколько, как бы это сказать, безумной, но ещё раз повторяю: в C89 она вообще невозможна. Так как там не бывает такого, что указатели равны, но один валиден, а другой — нет. И объекты в realloc не удаляются и не создаются. А всего лишь перемещаются: The
realloс
function changes thesize
of the object pointed to byptr
to the size specified by size. и Therealloс
function returns either a null pointer or a pointer to the possibly moved allocated space.Увы, нет никакого способа придумать как сделать эту программу невалидной в C89. Я с разработчиками компиляторов общался и некоторым количеством людей, которые в комитете по стандартизации заседают — никто не смог. Они честно пытались.
Максимум, чего удалось добиться — упоминания того, что C89 это очень старый стандарт (а ничего что ещё пару лет назад один очень распространённый компилятор ничего новее не поддерживал?) и сегодня не актуален, а для C99, видите, мы какую шнягу красивую придумали.
qw1
06.11.2021 13:20С одной стороны, я их понимаю — они держат в уме всякую Эльбрусо-подобную тегированную память, где указатель это не только смещение, но ещё может хранить и тип, и размер на указываемый массив. И два указателя на одну ячейку памяти могут быть не равными побитно.
С другой стороны, волосы на голове начинают шевелиться от того, что пишу я себе под Windows x64, никого не трогаю, скастил указатель в uint64_t для каких-то своих целей, а потом обратно в char*, и мне компилятор в полном своем праве ради шутки вставил Format C: (а нечего UB делать на ровном месте).khim
06.11.2021 16:26+5Да не держат они в уме всякие Эльбрусы. Забудьте.
Оптимизация, которая там делается (превратить
*q
в1
, а*p
в2
) в некотором смысле напрашивается. Но она опасна. Валидна только тогда, когда можно доказать, что*p
и*q
укаывают в разные места памяти.И, разумеется, это не новая проблема. Ещё во время первоначальной стандартизации C была сделана попытка добавить ключевое слово
noalias
, чтобы эту проблему решить.Возражения некоего частного лица привели к тому, что эту затею, в тот раз отставили.
В C99 к этой затее вернулись и добавили в язык
__restrict
со слегка другой семантикой.Однако это тоже, особо не сработало: люди редко исползуют
__restrict
и ещё реже — правильно.А к этому моменту уже ж разработали C++ и, что важнее, STL. Который, блин, оказался завязан на то, что компилятор может вот подобные как раз оптимизации делать — но без малейших объяснений того, как это сделать безопасно.
И вот тут-то кому-то из разработчиков компиляторов в голову пришла мысля (скорее всего не одному): у нас же в языке имеются десятки разных интересных UB (которые там появились по совсем другому поводу) и у нас есть разрешение в случае возникновения UB делать что угодно (такое разрешение, кстати, тоже появилось в C99, не в C89, некоторые даже приписывают соотвествующему изменению злой умысел, хотя на самом деле, почти наверняка, просто потому что ограничения на то, что такое UB, прописанные в C89, опираются на понятия, в нём не описанные и, конечно, гораздо проще их снять, чем формализовать). давайте их активно находить и тот факт, что они не выполняются — использовать (программа же их не исполняет, ей запрещено, значит код, вещущий к UB, тоже не исполняется, значит его можно выкинуть).
Начавшаяся после этого борьба брони и снаряда — почти привела к катастрофе: оказалось, что люди, в общем, когда программы пишут решают прикладные задачи и тесты гоняют, а не ребусы на тему “а нет ли у нас тут UB? а если покреативней поинтерпретировать — всё равно нет? а если воображение напрячь?”.
Оказалось, что, в общем, воображение у разработчиков компиляторов посильнее, чем у остальных программистов (см. обоснование безумия с
realloc
в C99).Но настоящая катастрофа произошла тогда, когда разработчикам компиляторов оказалось и этого мало, они выбили себе разрешение прописать себе в стандарт дополнительные UB (читаем внимательно: it would be desirable for the Standard to mean это ни разу не mean, это всего лишь позволение изменить стандарт, не признание того, что стандарт что-то там кому-то позволяет), но не сделав этого, начали этим фиговым листком прикрываться.
В результате мы получили язык для которого не существует ни одного современного компилятора! Да, вот так, просто: ни одного!
Поскольку разработчики компиляторов полагаются на то, что разработчики будут соблюдать запреты, которых в стандарте просто нету! Совсем нету, ни в каком виде!
Кстати ваш пример с преобразованием указателей в
intptr_t
и обратно как раз и является одним из камней предкновения. Ибо в соотвествии с буквой стандарта он валиден, а объявить его невалидным и сказать, что какой-нибудь XOR-связный список — невалидная конструкция у них наглости пока не хватает. А без этого у них никак не получается (уже второй десяток лет не получается!) придумать непротиворечивые правила работы с указателями, которые позволили бы имломать программы и дальшеделать те оптимизации, которые они уже делают.Вот такая вот ужасная драма, где никто, вроде, не виноват, а убытки исчисляются миллиардами (хорошо если не триллионами).
Проблема в том, что долгое время никто не мог придумать: что же, всё-таки, с этим делать. Понятно, что хочется как-то сделать так, чтобы в языке UB не было совсем (во многих других языках их нет: C#, Java, JavaScript и так далее), но было непонятно как удалить UB из языка без GC: у вас же, в таком языке, можно удалить объект, на который, где-то там ещё, есть “живые ссылки”! А можно ведь, ко всему прочему, из разных потоков, параллельно, память менять, там тоже разные процессоры много разного интересного может насчитать.
Решение Swift было промежуточным, почти как у языков с GC: а давайте мы сделаем малоинвазивный GC на счётчиках, тяжёлый рантайм ему не нужен, но безопасность можно обеспечить. А все опасные конструкции вынесем в стандартную бибиотеку.
Такой себе недо-managed язык. Если компилятор сможет, в некоторых местах, манипуляции со счётчиками извести — он может даже достаточно быстрым оказаться. Вроде как проблемы скорости всё равно не решились, впрочем (Apple, по итогу, сделала аппаратное ускорение в свои процессорах, но такое могут себе позволить себе не все).
А вот Rust сделал куда более интересную вещь: предложил отдельно пометить все места в программе, где ипользуются конструкции, которые могут вести к UB! И оказалось, что, при должном упорстве, можно сделать так, чтобы в большой программе так было пемечено где-то 1% года!
А в остальной программе пусть UB не будет (утечка памяти — это не UB).
Это было смелое решение, в которое, если честно, я много лет не очень верил. Смотрел на то, как они барахтаются, но не верил, что они могут сделать язык, близкий по выразительности и скорости к C++, но без UB (ну Ok, “почти без” UB, “минное поле” в 1% кода это куда лучше, чем “минное поле” в 100% кода).
Но судя по тому что происходит в последние год-два (поддержка крупными компаниями, попытки переписать драйвера в Linux на Rust и так далее) — они смогли.
Оперевшись на весьма интересное наблюдение: программисты на “современном C++” и без того очень часто используют std::unique_ptr и часто действуют в парадигме блокировки-чтения записи, когда у вас в программе есть, у каждого объекта, либо один писатель, либо много читателей (но писателей тогда нет ни одного, XOR а не OR).
Это позволяет решить проблему с UB, но некоторые конструкции оказываются невозможными (например банальную очередь создать уже нельзя).
С другой стороны — так ли часто вы пишите неблокирующие стеки и очереди? Я наблюдал как-то за этим увлекательным процессом. Там на примерно 100 строк кода ушло две недели написания, а количество строк с описанием было примерно на порядок больше, чем кода. И это после того, как описание сильно упорядочили и порезали (добавив ссылок на несколько статей).
Уж если вам что-то такое захочется ещё раз сделать — добавить туда немного меток
unsafe
проблемой явно не будет.qw1
06.11.2021 19:59Если хочется обсудить проблемы с UB, то приводить realloc в пример — так себе идея. Уже и не помню, когда в последний раз им пользовался.
Как я понял из вашего текста, вот это — UB
А вот это — корректноvoid* q = realloc(p, newsize); if (p == q) { ... }
Странно конечно, но не смертельно.intptr_t p1 = static_cast<intptr_t>(p); void* q = realloc(p, newsize); if (p1 == static_cast<intptr_t>(q)) { ... }
Мне вообще мало интересны особенности стандартной библиотеки. Чаще пользуюсь или своими обёртками, или фреймворками, или функциями ОС.
khim
06.11.2021 20:41+3Если хочется обсудить проблемы с UB, то приводить realloc в пример — так себе идея.
А почему, собственно?
Уже и не помню, когда в последний раз им пользовался.
А какая разница? Нам же важно понять: имеет ли смысл та логика, которой руководствуются компиляторы или нет.
В математике для этого обычно берутся вырожденные примеры.
realloc
— как раз такой. Подробно описан в стандарте, но редко пользуется, потому сослаться на то, что эта горе-оптимизация ломает важну программу нельзя.А вот это — корректно:
intptr_t p1 = static_cast<intptr_t>(p); void* q = realloc(p, newsize); if (p1 == static_cast<intptr_t>(q)) { ... }
Ух ты, прелесть какая. А вот совершенно неясно - оно корректно или нет. Нет, clang считает, что так тоже нельзя. Хотя тут мы вообще не используем ужасный, кошмарный, “отравленный” указатель и сравниваем просто числа.
Собственено ровно в эту проблему и упёрлись разработчики Rust: выяснилось, что правила, которыми руководствуется компилятор внутренне противоречивы и их необдуманное применение может ломать даже самые простейшие программы без всяких
realloc
'ов и прочей мути.Ровно по этой же причине, кстати, не удаётся и добавить pointer provenance в стандарт с 2004го года. Просто никому не удаётся придумать такие правила, которые бы, с одной стороны, позволяли разработчикам компиляторов производить все те оптимизации (ну или большинство, хотя бы) , которые они напридумывали, а с другой — не приводили бы к тому, что масса ну совершенно очевидно правильных программ не оказались бы поставлены “вне закона”.
Чаще пользуюсь или своими обёртками, или фреймворками, или функциями ОС.
О! У меня для вас хорошая новость! В соотвествии с “креативным” прочтением стандарта у вас вообще не может быть такой функции, как
mmap
илиmunmap
. Совсем. Никогда.Потому, собственно, описание
malloc
/calloc
/realloc
/free
и оказалось частью стандарта, что их написать на стандартном C в принципе нельзя.Ибо только вот первые три функции могут создавать области памяти, к которым может обращаться корректная программа, а последние две — удалять.
Они могут это делать потому, что являются частью стандарта и, соотвественно, могут делать то, что никакие другие функции делать не могут.
А никакие другие способы, не сводящиеся к этой четвёрке, невозможны и, соотвественно, функции map/unmap (и их аналоги в Windows) — вызывать, в корректной, программе, нельзя.
Правда, как великая уступка разработчикам, компиляторы реальные программы, вызываеющие эти функции стараются не ломать. Но это так — подачка “немытой толпе” (вернее боязнь того, что уж настолько безумный компилятор всё-таки люди не захотят использовать), стандарт позволяет безвозбранно любые подобные программы крушить.
Вы счастливы?
qw1
07.11.2021 00:43Поигрался с компилятором. Видимо, всё это происходит от того, что с malloc/free есть очень агрессивные оптимизации. Если я указатель не возвращаю из функции, компилятор может вообще не вызывать никакие библиотеки, а разместить значение на стеке или даже в регистрах. И если я «забуду» сделать free, даже утечки памяти не будет.
Странность с realloc видимо нужна, чтобы в глубинах компилятора моделировать его как malloc+memcpy+free. А malloc всегда возвращает новые адреса, ни с кем не пересекающиеся.Потому, собственно, описание malloc/calloc/realloc/free и оказалось частью стандарта, что их написать на стандартном C в принципе нельзя
Почему нет?char heap[100500*1024];
— и откусывай оттуда своими аллокаторами.
Соглашусь, приведённые вами примеры очень континтуитивны и генерируют очень много wtf на строку кода. Но в реальной жизни никто не захочет сравнить указатель с чем-то после free/realloc. Опять же, это только проблема free/realloc, а реально пользуются API/фреймворками, в C++ вообще этого нет (нет realloc в парадигме new/delete — нет проблемы).
khim
07.11.2021 01:23+4Почему нет?
Потому что C - это не C++.
char heap[100500*1024];
— и откусывай оттуда своими аллокаторами.В C++20 это сработает благодаря PR593. Только не забудьте правильные конструкторы/деструкторы вызывать. Конечно вы получите, таким образом не
malloc
/free
, аnew
/delete
, но для C++ этого хватает.А вот в C (и более ранних версиях C++) — не получится, потому что нет никакого способа превратить кусок этого массива в объект другого типа и, главное, нельзя заставить этот объект перестать существовать.
Можно, наверное, работать только и исключительно с
char
'ами, но это, как бы сказать помягче, дико неудобно.Опять же, это только проблема
free
/realloc
, а реально пользуются API/фреймворками, в C++ вообще этого нет (нетrealloc
в парадигме new/delete — нет проблемы).Конкретно этой проблемы нет, зато есть масса других. Одно существование std::launder — много говорит о масштабах разрухи.
Как я уже сказал: проблема не в том, что “правила игры”, навёрнутой вокруг этого всего, сложно соблюсти, проблема в том, что никто не может их даже внятно сформулировать!
Известно только, что то, что прописано в стандарте — это “не то”, то есть соблюдения стандарта при написании программ — недостаточно.
P.S. С точки зрения нормальных людей было бы разумно, понятно, трактовать все эти эффекты консервативно, разрешая оптимизации в отдельных местах, помеченных как-нибудь особо. Но тогда упадёт скорость на бенчмарках, а этого разработчики компиляторов, конечно, допустить не могут. Но чёрт бы с ними, дали бы ключик -fno-provenance (как уже дали -fno-strict-aliasing)… но нет, не хотят. Слишком сложно, говорят. А избегать-того-не-знаю-чего — раз плюнуть, да?
Mingun
07.11.2021 08:18Ух ты, прелесть какая. А вот совершенно неясно — оно корректно или нет. Нет, clang считает, что так тоже нельзя.
Вы несколько подменили пример. Если новый адрес записывать в другую переменную,
то выводитсяХотя нет, я забыл поменять вывод. Если выводить то, что сравнивается, то все равно разное.1 1
.В общем, проблемы о того, что разработчики стандарта тихо-мирно поменяли смысл сравнения указателей с побитовой эквивалентности на логическую эквивалентность и под капотом сравнивают не все биты, чтобы заявить, что указатели одинаковые. Вот и получается, что 0b10011 равно 0b00011. Как я понимаю, в стандарте явно этой логики не прописано, а все какими-то окольными путями и полунамеками.
khim
07.11.2021 16:48+2В общем, проблемы о того, что разработчики стандарта тихо-мирно поменяли смысл сравнения указателей с побитовой эквивалентности на логическую эквивалентность и под капотом сравнивают не все биты, чтобы заявить, что указатели одинаковые.
Если бы всё было так просто. Тут не в битах дело. Почитайте пропозал-то: они статически, во время компиляции, пытаются доказать, что некоторые указатели указывают на разные объекты в памяти.
И вот тут у них у самих неразбериха: в соотвествии с “креативным” прочтением правил указатель p протух, ладно, но p1 — это ж число, оно, вроде как, протухать не должно… или должно?
Вот если оба указателя превратить в числа, а потом обратно — clang, наконец-то, перестаёт издеваться над здравым смыслом. Но это законно или нет? Почитайте к чему все эти попытки ведут.
Правил нет, но вы держитесь. В смысле — их соблюдайте.
qw1
07.11.2021 19:29Вот если оба указателя превратить в числа, а потом обратно — clang, наконец-то, перестаёт издеваться над здравым смыслом
У меня не перестаёт, наблюдаю вывод: 1 2
mayorovp
07.11.2021 10:17А никакие другие способы, не сводящиеся к этой четвёрке, невозможны и, соотвественно, функции map/unmap (и их аналоги в Windows) — вызывать, в корректной, программе, нельзя.
Вообще-то можно, и именно потому, что компилятор про эти функции ничего не знает. Он не может "испортить" тот код, про который ему ничего не известно.
Проблема наступит в тот момент, когда эти функции станут известны компилятору ради очередных оптимизаций.
qw1
07.11.2021 16:02А представьте, что мы свою программу не динамически линкуем с HeapAlloc/HeapFree, а пробуем написать целую ОС и приложения в ней одним большим блобом. Так, что компилятор на момент компиляции всё про всех знает и может оптимизировать, как посчитает нужным. И вот тут можно очень хорошо отгрести.
khim
07.11.2021 17:47Это, кстати, типичная ситуация для “малого” embedded, где собирается не программа, а прошивка.
И где компилятор, действительно, при использовании LTO знает всё про всю программу.
khim
07.11.2021 16:54+1Вообще-то можно, и именно потому, что компилятор про эти функции ничего не знает.
Это не определение корректной программы, извините. Корректная программа не должна зависеть от знания компилятором чего-либо и создавать объекты иначе как через malloc/calloc/realloc и удалять иначе как через realloc/free.
В C++ ещё new/delete появляются.
Проблема наступит в тот момент, когда эти функции станут известны компилятору ради очередных оптимизаций.
Совершенно верно, но это, собственно, и означает, что программа всегда была некорректной.
yeputons
07.11.2021 11:24+1> Если уж два указателя равны, так они таки равны. И либо оба можно использовать, либо оба нельзя использовать.
Так они же не "равны", они "compare equal", если я правильно понял. То есть верно `p == q`. А то, что их можно при этом одинаково использовать, вроде нигде не написано?
khim
07.11.2021 17:13+3А то, что их можно при этом одинаково использовать, вроде нигде не написано?
Написано. В C89 написано вот это
If two pointers to object or incomplete types compare equal. they both are null pointers. or both point to the same object. or both point one past the last element of the same array object.
Четвёртый вариант (и связанное с ним безумие, когда у вас есть два равных указателя, но один можно использовать, а другой нельзя) появляется только в C99.
Об этом, мне, собственно, заявили прямо:
If you believe pointers are mere addresses, you are not writing C++; you are writing the C of K&R, which is a dead language. The address space is not a simple linear space: it is a time-varying disjoint sum of many small affine spaces, which the compiler maps to a linear address space (for common target triples!). This has been in the standards since C99 (although perhaps not very clear for those not versed in standardsese).
И про то, что безумие началось с C99 (попытки делались ещё при разработке C89, но Керниган, своим авторитетом, их отбил) и про то, что правила у нас явно не прописаны ни в одном стандарте (причём с точки зрения разработчиков компиляторов это небольшая проблема… не им же эти, нигде не описанные правила, соблюдать) и про многое другое — было сказано явно.
Рекомендация была такой:
Viewing C/C++ as static is a mistake. Viewing it as a portable assembler is a mistake. Viewing it as "GCC behaved this way last I checked, so it still holds, right?" is also a common mistake. It is important to stay up to date on the standards, be aware of what fussy code looks like (or, practically, what not-fussy code looks like), and know how to ask for help, as I have stressed in each of my previous replies.
Офигительный подход как для языка основное достоинство которого — тот факт, что на нём написаны миллиарды строк кода, не так ли?
Теоретически я ешё как-то могу применить этот подход, благо у нас есть и разработчики clang в компании и весь код регулярно прогоняется через новые его версии (те, которые ещё в разработке), но, блин, они реально хотят сказть, что все разработчики, использующие C/C++ должны подобным заниматься? Опираться даже не на стандарт, а на what fussy code looks like (or, practically, what not-fussy code looks like).
Да они оху…, нет, охр… да блин, как на это без мата-то реагировать?
TargetSan
05.11.2021 12:41+1- Любая синтаксически корректоная программа, написанная пусть даже и 20 лет назад обязана компилироваться.
При этом от эпох комитет пренебрежительно отмахивается.
Swift (у Apple)
ЕМНИП там везде принудительный подсчёт ссылок. Любой системщик мгновенно поднимет на вилы. Плюс за пределами экосистемы эппла распространение стремится к нулю.
khim
05.11.2021 16:25+2При этом от эпох комитет пренебрежительно отмахивается.
Если бы они только от эпох отмахивались. Они даже от предложений не ломать существующие программы, написанные в соответвествии с существующими стандартами (типа такого) отмахиваются.
Сложно, говорят, нам такой компилятор написать. Мы уже заложились на то, что разработчики правил pointer provenance не нарушают. А ну и что, что мы уже 20 лет не можем придти к единому мнению о том, как эти правила, всё-таки, должны работать.
Пофиг. Пусть машину времени купят, слетают в 2050й (или таки в 2100й?) год, узнают, что там мы таки, в конце-концов, придумали, вернутся назад и соблюдают.
Что? С покупкой машины времени затык? Не наши проблемы.
ЕМНИП там везде принудительный подсчёт ссылок. Любой системщик мгновенно поднимет на вилы. Плюс за пределами экосистемы эппла распространение стремится к нулю.
Со счётчиком ссылок компилятор неплохо борется. Со временем будет лучше. C++ так же развивался. Но то, что никто, кроме Apple, Swift не поддерживает — проблема, да.
Google немного поигрался, но забил, в конце концов.
Но это древняя традиция, ещё с прошлого века. Когда все переходили с C на C++ у Apple тоже был свой язычок — Objective C. Многие решения в Swift вызваеы необходимостью сосуществования с модулями на Objective C, а вне экосистемы Apple это никому не нужно, так что так и получается: у Apple — Swift, у всех остальных — Rust.
sim31r
05.11.2021 15:30Практически таковых есть два
Компилируемые языки наверное имеются ввиду.
khim
05.11.2021 17:36+3Языки, способные заменить C/C++ хотя бы в теории.
Это довольно много ограничений: например C/C++ популярны в embedded, где невозможно использовать большую стандартную библиотеку или, тем более, GC.
Также, разумеется, туда не лезет JIT, а интерпретатор не годится из-за потребления ресурсов.
Нужно подмножество стандартной библиотеки, которая не аллоцирует память (иначе у вас с реальным временем проблемы полезут)
То есть речь идёт даже не просто о компилируемых языках, но о каком-то ограниченном подмножестве.
Долгое время сколько-нибудь популярных альтернатив для C/C++ просто не было (почему, собственно, разработчики компиляторов и могли вытворять то безобразие, которое они вытворяли: типа, всё равно ж будете жрать кактус, куда вы денетесь?).
Сейчас они появились, но их, мягко скажем, немного.
P.S. Строго говоря Rust и Swift не являются единственной альтернативой. Есть ещё D, Zig и внушительное количество ещё более экзотических языков. Но в случае с D такая возможность остаётся чистой теорий (теоретически без GC можно обойтись, практически же вы, в этом случае, не сможете использовать почти ничего из уже написанного кода), а больше в первой сотне языков на TIOBE я таких языков не вижу (хотя может чего и упустил, там есть много языков, о которых я мало чего знаю).
northzen
05.11.2021 17:44А что насчет Zig?
На мой вкус у него адекватная идеология развития. Судя по тому, что я читал, авторы знаеют, чего от языка хотят. Но такого комьюнити как у Rust у них нет, конечно же.khim
05.11.2021 18:47+3Как я уже сказал есть масса разных языков разной степени экзотичности. Zig пока что один из них.
Пока неясно — будет ли он развиваться или повторит судьбу бесконечных диалектов Nim, Oberon и прочей экзотики.
Rust сегодня поддерживают Amazon, Facebook, Google и Microsoft — то есть практически все крупные игроки, кроме Apple (у которых свой Swift есть и который они, конечно, не будут на Rust менять).
При этом, насколько я знаю, никто из них не решился пока перейти с C++ на Rust, но все они, по крайней мере, думают о такой возможности (а некоторые уже и планы перехода строят).
А для успеха языка поддержка его “большими” игроками очень много значит.
Потому практически, как я сказал, выбор между Rust и Swift сегодня. Завтра (образно: лет через 10, на практике-то) ситуация может и измениться.
AnthonyMikh
08.11.2021 13:43А что насчет Zig?
У него, как и у C, идеология "Программист Знает Что Делает". Для меня это крайне существенный минус: как показывает практика, доверять мешкам с мясом нельзя.
sim31r
07.11.2021 00:33-1Это довольно много ограничений: например C/C++ популярны в embedded, где невозможно использовать большую стандартную библиотеку или, тем более, GC.
Уже бодро Java прописался в embedded, как появились мелкие устройства помощней сразу стал нормой.
khim
07.11.2021 00:51+1Уж так прям и нормой? А ничего, что Raspberry PI выпускается тиражом меньше десяти миллионов штук в год, а рынок микроконтроллеров - это больше десяти миллиардов штук в год?
Даже если присовокупить сюда китайские клоны — всё равно речь идёт хорошо если о нескольких процентах рынка. Ибо подавляющее число встраиваемой электроники JVM запустить не способна и никогда не будет способна.
Вы бы лучше про MicroPython вспомнили. У него требования полегче, можно где-нибудь на 5-10% встраиваемых систем запустить.
Но как только мы выходим за рамки штучных поделок — так сразу смысл его использования теряется. Так как платформы, способные запустить что-то на MicroPython всё равно в разы дороже, чем те, которые поддерживают C/C++/Rust.
Вот ещё более мелкие платформы, где даже этого нельзя использовать и есть только ассеблер — они всё ещё куда более популярны, чем платформы с поддержкой Java… но их время уже потихоньку уходит.
sim31r
07.11.2021 03:46Микропитон по впечатлениям удел DIY, для хобби проектов. Серьезные проекты, прошивка роутера какого-нибудь уже на Java.
Микроконтроллеры тоже всё более мощные становяться, какой-нибудь STM3H7 заметно мощнее первых пентиумов. Но где-то, конечно, ставят мелкие МК с десятками байт оперативной памяти и этого достаточно.
khim
07.11.2021 04:06+1Серьезные проекты, прошивка роутера какого-нибудь уже на Java.
А пример можете привести? Потому что максимум чего я видел — Java-applet для управления и настроек. Но он на самом роутере не исполняется, он в браузере на компьютере работает.
А так-то можно много чего записать в малинку, даже Windows, как известно, бывает и даже не только IoT. Но это тоже всё DIY.А насчёт нормальных проектов… насколько оно на практике востребовано? Какая-нибудь статистика, подтверждающая ваши утверждения, есть?
Ну и, понятно, есть ещё такая штука как размытие самого понятия Embedded. Формально же компьютер Tesla, на котором Cyberpunk 2077 бегать может - тоже embedded, но там кроме этого компьютера ещё десяток других, попроще.
Микроконтроллеры тоже всё более мощные становяться, какой-нибудь STM3H7 заметно мощнее первых пентиумов.
А на первых пентиумах Java игрушкой была. То есть, в те времена, даже офисные пакеты на Java пытались портировать, только не взлетело это нифига.
Но где-то, конечно, ставят мелкие МК с десятками байт оперативной памяти и этого достаточно.
И тут тоже интересно: какой процент вот этого всего? Потому что есть ощущение, что цена 32-битных микроконтроллеров снизилась настолько, что 8-битные или там 4-битные больше смысла не имеют (в какой-то момент цена всё равно упирается в корпусировку и монтаж, так что ниже какой-то цены у вас никакой контроллер опуститься не может).
Я знаю только про прикольное сравнение микроконтроллеров из USB-зарядок с компьютером Apollo, но там, всё-таки, не самые дешёвые контроллеры пользуют.
sim31r
07.11.2021 23:29-1Знакомые на Java пишут ПО для контроллеров управляющим освещением на автомобильных трассах, для учета потребления электроэнергии. Исполнительное устройство не микроконтроллер, что-то вроде RPI, простенькая плата. Java выбрали потому что основной момент коммуникации с внешним миром, нужно быть всегда в онлайне при использовании всех доступных каналов связи. Даже если подключено оптоволокно, есть еще GSM модем на всякий случай. И даже если нет связи, ПЛК автономно будет собирать статистикой и работать по расписанию, синхронизируя расписание с сервером через некую виртуальную модель поведения. На С++ такие логические абстракции писать уже сложнее.
Я знаю только про прикольное сравнение микроконтроллеров из USB-зарядок с компьютером Apollo, но там, всё-таки, не самые дешёвые контроллеры пользуют.
В обоих примерах выполняется принцип достаточности. На Apollo тоже вычислительные системы были достаточные. Сложности основные были связаны не с ними. Вот пример ошибок в ИТ индустрии, в основном человеческий фактор сказывается, ошибки программистов. Проблем что где-то быстродействия не хватило не было.
TargetSan
05.11.2021 12:38+1Для плюсов альтернатива нужна не только в языке, но скорее в нормальном менеджере пакетов, например, и прозрачной системе сборки.
ИМХО этого не произойдёт никогда. Слишком много разных систем сборки и пакетных менеджеров с разными идеологией и подходами к решению тех или иных проблем. Комитет, даже если бы захотел какой-то унификации, мгновенно потонул бы в перетягивании одеяла в десятке разных направлений.
iShrimp
05.11.2021 18:23+1Новый язык программирования должен быть удобным, безопасным и производительным.
Ему нужно одновременно быть достаточно абстрактным и достаточно близким к железу.
Аппаратные платформы постоянно совершенствуются и обрастают различными наборами инструкций, которые призваны ускорить работу программ. Но компиляторы (GCC, Clang) по-прежнему плохо справляются с автовекторизацией и не используют даже половины новых возможностей. Также в стандартных библиотеках нет многих битовых манипуляций (rotl и rotr появились только в С++20). Поэтому приходится писать код на интринсиках отдельно для каждой платформы. А в каком-нибудь ЯВУ есть универсальная кроссплатформенная библиотека всех возможных векторных и битовых операций?
Многие алгоритмы поддаются оптимизации, если известен диапазон значений переменных. Например, если для вычисления значений цвета использовался массив uint32 и конечные значения лежат в диапазоне [0; 255], то их можно обрабатывать как uint8. Но компилятор этого не знает, пока мы сами ему не сообщим или не реализуем упаковку байт вручную. В каком-нибудь языке есть аннотации диапазонов значений переменных или массивов?
Mingun
05.11.2021 18:53Во всех типизированных. Конечно, количество информации в аннотациях варьируется :). Насколько я знаю, в Паскале можно ограничивать числовые типы до меньших диапазонов значений.
qw1
05.11.2021 22:46Это не то, что нужно в вышеописанной задаче.
В Паскале можно использовать тип0..255
, но нельзя сказать компилятору, что значение этого типа принудительно лежит в 32-битной переменной.
dolfinus
05.11.2021 01:09+23Если требуется объявить идентификатор, совпадающий с ключевым словом
Перегрузка переменных
кортежи могут неявно раскрываться и наоборот, неявно формироваться
заявлена поддержка перегрузки по возвращаемому значению
Больше. Неявных. Способов. Выстрелить. В. Ногу.
DustCn
05.11.2021 04:41+24Оператор возведения в степень
Во многих языках пытаются его ввести, и везде придумывают разные операторые символы (^, ** и т.п.). На этот раз обратный слэш.
Отвратительно, чтобы пользователь вглядывался и ломал голову куда ж там завален слэш...
Geenwor
05.11.2021 11:03+83.141_592 654; // floating constant
Как вот это понять? Что здесь есть "654"?
NeoCode Автор
05.11.2021 15:01при копипасте примеров из pdf иногда пропадают некоторые символы. Что-то я довводил вручную, но не везде заметил. Там должно быть подчеркивание.
Mingun
05.11.2021 15:19+2Вы пройдитесь и по остальным примерам тоже, там во многих местах явно такие же ошибки копирования (вроде непонятных чисел после точки с запятой — видимо, номер страницы или ещё что-то).
izvolov
05.11.2021 11:04+7PI = 3.141597
У вас ус отклеился.
По сути: дико запутанно и неудобно. Непонятно, кто и зачем будет пользоваться этой мешаниной.
bolk
05.11.2021 12:08-3with взят скорее из ДжаваСкрипта, а не из Пайтона.
static_cast
05.11.2021 12:32+3Да нет же, - классический Pascal. Кстати, возможно это действительно было бы удобно в случае ручного заполнения всяких C-подобных WinAPI-шных зубодробительных структур типа PIXELFORMATDESCRIPTOR.
amarao
05.11.2021 12:35+2А как там с type aliasing? видимо, как в Си. Нельзя, но если использовал, то у тебя больше не программа на Си.
Amomum
05.11.2021 13:58+2>Одну структуру можно встроить в другую так, как это реализовано в Go (и даже лучше - используется ключевое слово
inline
). Это очень простая и в то же время мощная концепция, прямо готовая для proposal'а в очередной стандарт С и/или С++... Удивительно - почему ее сразу не сделали в Си?В С++ это уже давно есть, только называется немножко иначе - наследование :)
chnav
05.11.2021 14:56Тоже про это подумал, но inline в данном случае позволяет поместить под-структуру в любом месте структуры, а наследование только в начале. Как по мне, подобный inline - очень удобная фича.
PS: мне дозволено отвечать только раз в сутки, так что можно сказать для меня это фича дня ))) приходится выбирать, где ответить )))
NeoCode Автор
05.11.2021 14:58+2Не совсем. При наследовании базовый класс всегда размещается в начале памяти производного. Здесь же можно разместить базовые классы в любом месте, по любому смещению относительно начала производного. Для низкоуровневых задач это бывает весьма полезно.
firehacker
05.11.2021 17:39+1Базовых классов может быть несколько.
struct PERSON_DATA { int Age; int Weight; }; struct LIST_ELM { void* Prev; void* Next; }; struct PERSON_ELEMENT : LIST_ELM, PERSON_DATA {};
PERSON_ELEMENT унаследован от PERSON_DATA, при этом PERSON_DATA не находится в самом начале PERSON_ELEMENT.
qw1
05.11.2021 22:52+1Для низкоуровневых задач это бывает весьма полезно
Слабо себе представляю пример, где это может быть полезно. Либо класс предназначен для реализации какой-то абстракции или логики, и там может быть полезно, чтобы композитный класс C был синтаксически совместим с его компонентами A и B.
Либо класс предназначен для маппинга на структуру в файле / в памяти, только тогда имеет смысл указывать вручную смещение каждого поля.
Но записывать что-то сложное с логикой в файл, да ещё при этом требовать попадание каждого поля в нужное смещение — какая-то фантазия.
Dukarav
06.11.2021 13:59+1Эх, молодежь!...
Почитайте описание конструкции "like" в структурах PL/1. Не прошло и 55 лет, как опять придумали "удобную фичу".
moshamiracle
09.12.2021 00:00А Вы раньше писали, что у вас много материалов для написания собственного языка 15мб текста в репозиториях. Это закрытые материалы или где-то можно их поизучать?
perfect_genius
05.11.2021 18:52+2В своё время удивило, что внутри switch нельзя goto к case из кода другой case в этом-же switch, хотя по сути это метки для goto-прыжка. Пришлось писать явно.
Здесь этого тоже нет?
DoctorRoza
06.11.2021 19:47Лучше бы очередной фреймворк для JS сделали. И то больше бы пользы было для пацанов.
Ritan
Уже был один язык, куда добавили абсолютно всё - PL/I. И что-то он сейчас не очень популярен
NeoCode Автор
Важно не то, сколько добавить, важно то — как это сделать. Т.е. не количество фич само по себе, а внутренняя взаимогармоничность разных элементов языка. Этого я у многих современных языков не вижу.
Ну а С∀ — просто интересный образец того, как люди видят недостатки в существующем и пытаются их как-то исправить. Здесь интересно само направление мысли, что-ли… Вот например ссылки. На Хабре уже была статья о недостатках ссылок, с предложением о том как эти недостатки устранить. Здесь — другой подход (тоже не лишенный недостатков!). Но интересно сравнить, обдумать…
Ritan
Понятное дело, что добавить можно по разному.
Но вот это
- это взрыв на скобочной фабрике.
А вот это уже начинает напоминать регулярки.
А также перегрузка ключевых слов, потоки зачем-то в виде builtin типа.
Синтаксис дефолтных аргументов - мрак. Любая попытка посчитать запятые и понять, какой аргумент куда идёт, будет вызывать ненависть со стороны читателя.
Синтаксис объявления переменных мне нравится( т.е. консистентный и фиксированный порядок появления const и т.д. Но раз уж совместимость с C всё равно сломана, то зачем тащить за собой остальные странности.
В целом, от языка создаётся ощущение, что в него тащили всё, что попадалось на глаза не сильно задумываясь над тем, а как этим будут пользоваться( и можно ли этим пользоваться, не стреляя себе в ноги вообще ). Т.е. даже на фоне плюсов это выглядит монструозно
Не поймите меня неправильно - эксперименты это хорошо и здорово. У меня тоже есть свой игрушечный язык, которым никто не будет пользоваться. Просто такие попытки исправления C заранее обречены на провал
anton19286
Разве что генераторы забыли.
Ritan
Как это забыли? Они же там тоже есть