[ 19 ] :: ФункцииК функциям языка C++ у меня нет серьёзных претензий кроме следующих. Все эти претензии не сильно мешают создавать высокопроизводительный код, а мешают, скорее, делать его более понятным и удобным.
Во-первых, несколько раздражает необходимость повторять тип каждого аргумента при объявлении функции.
Код:
void f ( int a, int b, int c, int d ) {
...
}
Более удобно было бы
Код:
void f ( int a, b, c, d ) {
...
}
// или
void f ( a, b, c, d : int ) {
...
}
Конечно, это не избавит от необходимости всё равно указывать типы перед каждой переменной, если они чередуются или разные:
Код:
void f ( int a, double b, float c, char d, int e ) {
...
}
В качестве альтернативного варианта определения типов можно заставить определять их как локальные переменные:
Код:
void f ( a, b, c, d ) {
int a, d;
double b, d;
...
}
Ведь эти переменные всё равно будут локальными.
Во-вторых, несколько более сильно раздражает невозможность определить функцию внутри функции.
Код:
void getNeighbors ( const Point & p ) {
bool areNeigbors ( const Point & a, & b ) {
return ...
}
...
}
В-третьих, вместо функций иногда приходится создавать функторы, то есть классы, в которых переопределена операция «круглые скобки», чтобы смотреть на функцию как на класс. Зачем? Не только для того, чтобы можно было передавать её в качестве аргумента (для этого можно использовать указатель на функции), но и для того, чтобы можно было пользоваться деструктором. Бывает, что возникает следующая неприятная ситуация:
Код:
int f ( ... ) {
int * a = new int [ ... ];
int * b = new int [ ... ];
...
if ( всё плохо ) {
delete [ ] a;
delete [ ] b;
return 1;
}
...
if ( всё ужасно! ) {
delete [ ] a;
delete [ ] b;
return 2;
}
...
// Всё хорошо
delete [ ] a;
delete [ ] b;
return 0;
}
Явное дублирование кода. Данная проблема, конечно, решается с помощью goto и переменной, сохраняющей результат. Но ещё удобнее затолкать освобождение памяти в деструктор, и когда функтор уничтожается, вся память нормально освобождается. Не нужно перегружать тело функции обслуживающими операциями. Однако беда в том, что процедуры выделения памяти всё равно приходится прописывать в теле функции. Было бы удобно сочинить способ, в результате которого служебные процедуры в начале и в конце функции прописывались где-нибудь не в её теле.
В-четвёртых, так называемые «лямбды», появившиеся в C++11, жрут память. Я не проверял, как и зачем они это делают, но создать рекурсивную лямбду внутри другой функции, да ещё и с большим количеством захватываемы параметров – всё равно что убить стек. Обычная функция в этом смысле сильно выигрывает. «Лямбда» - это всё-таки объект, но объект очень неэффективный для рекурсивного использования. Если можно сделать так, чтобы он стал эффективным, то было бы прекрасно. «Лямбды» должны быть в языке, очень удобно указывать, например, критерий сортировки, не создавая отдельную для этого функцию. Если бы ещё синтаксис был попроще. Вот, например, мы хотим заполнить матрицу числами по простому правилу. Почему бы не сделать это так:
Код:
auto А = Matrix < int32u > ( m, n, (i,j)->i+j );
В-пятых, функция должна возвращать не только объект одного класса, но целый кортеж. Это замечание относится скорее уже не к функциям, а к типам данных. Должен быть тип, позволяющий эффективно работать с кортежами и писать что-то типа
Код:
( int, float ) getBestDistance ( int fromVertex ) {
...
return ( bestVertex, bestDistance );
}
При этом должна быть организована удачная семантика перемещения, наподобие r-value references в C++, когда вместо копирования одного объекта с последующем удалением оригинала он перемещается на нужное место с минимальными затратами.
Уже другой разговор, как обходить заведомо неоптимальные конструкции типа желания обменять переменные местами:
Код:
(a, b) = (b, a);
Но главное, чтобы из-за необходимости возвращать несколько значений не приходилось бы дописывать аргументы-ссылки или указатели при вызове функции.
В-шестых, должен быть какой-то более удобный механизм определения функции с переменным числом аргументов. Какие-то ключевые слова, которые позволяли бы без лишней суеты обратиться к нужному аргументу:
Код:
void f ( . . . ) {
int x = argv [ 0 ];
int y = argv [ 1 ];
...
}
Здесь огромное количество проблем. Самая первая проблема в том, что компилятор не может определить типы, а, следовательно, убедиться в правильности тела функции:
Код:
int a, b;
double c, d;
f ( a, b ); // Нормально
f ( c, d ); // Ошибка
Чтобы понять, где нормально, а где ошибка, компилятор должен знать всё о теле функции, то есть она должна быть в той же единице трансляции. А если ещё и дать возможность делать такие штуки:
Код:
void f ( . . . , int a, . . ., double b ) {
...
}
То есть дать программисту возможность сказать, что где-то «в середине» один из параметров должен быть int, а последний должен быть double, то мы даём разработчику компилятора задачу, возиться с которой ему будет не проще, чем с любой труднорешаемой задачей. Компилятор станет экспоненциальным по сложности, даже если не добавлять возможность указать значение параметров по умолчанию.
С точки зрения безопасности, я не знаю, как организовать функцию с переменным числом аргументов. Если только не макросами, которые раскрываются до компиляции. Либо эти функции должны быть написаны на другом языке, допускающем что-то типа void *, когда приведение типов ложится на программиста. Ну, или динамическая типизация... однако я считаю, что в HPC-языке типизация должна быть статической.
В-седьмых, должен быть тип данных «функция» и функции должно быть можно передавать не через указатель, а как объект. Я не вижу прямо острой необходимость перечёркивать концепцию указателей на функции из Си, но считаю её неудобной. Функция может быть объектом, и можно передавать его как по значению, так и по ссылке или через указатель. Фактически, имеет смысл объединить функции и «лямбды» в одно целое, но сделать это эффективно, чтобы функции, которые «не лямбды», не занимали бы памяти, как её будет занимать «лямбда». Мне лично не нравится, что у нас получился разный синтаксис для обычных функций и для «лямбд». Думаю, всё можно объединить одним способом и различать по контексту.
Далее. Программист должен управлять возможностью делать функцию встроенной (inline) или нет (notinline). Эти требования должны быть не рекомендацией компилятору, а жёстким требованием, и если он не может его выполнить, то должен умереть от невообразимого чувства вины за себя и своего создателя. Либо хотя бы сообщить об ошибке (например, при рекурсивных inline-функциях с непонятным исходом, которые фиг пойми как встраивать).
Более мелкие замечания у меня тоже есть, но они скорее баловство. Например, параметры по умолчанию хотелось бы организовать так, чтобы они могли быть в середине списка аргументов:
Код:
void f ( int a, int b = 0, int c ) {
...
}
// Вызов
f ( 2, , 3 ); // Второй параметр пропускаем, показывая, что берём значение по умолчанию
Аналогично, замечание по стилю определения. Для математика удобнее смотреть на функцию так:
Код:
makeMeHappy : double m, e -> int H, a, p, p, y {
...
}
Но это всё уже мелочи, привыкнуть можно, в принципе, к любому стилю, лишь бы он не был извращением.
[ Продолжение следует ]