Comments 56
Для C++ еще есть известный пазл: константный указатель/указатель на константу
int *const p1
int const* p2
const int* p3
+10
Существует простое правило:
const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке. В этом случае, очевидно, модифицирует то, что прямо после.
И сразу легко понять что
это константный указатель на указатель на константный int.
const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке. В этом случае, очевидно, модифицирует то, что прямо после.
И сразу легко понять что
const int ** const p4
это константный указатель на указатель на константный int.
+4
Существует простое правило:
const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке.
Хаха, это простое правило в стиле C++, а в стиле C правило звучит так «надо прочитать объявление в обратную сторону».
const int *const p; // p - это константный указатель на int костантный
const int *const *p; // p - это указатель на константный указатель на int костантный
const int **const **p; // p - это указатель на указатель на константный указатель на указатель на int константный
+2
ЕМНИП синтаксис объявлений в C был сделан так, как удобнее разбирать компилятору. В Паскале же наоборот, как удобно читать человеку, за счет усложнения компилятора. В Go, видимо, нашли некий компромисс. :)
-2
Как раз наоборот: когда компилятор C++ строит AST и встречает int name..., он не знаете что будет дальше — объявление переменной или функции, поэтому эту информацию надо или запоминать где-то или возвращаться назад по исходнику. К тому же для языков с подобными грамматиками сложнее программировать восстановление после сбоев во время синтаксического анализа. Для паскаля как раз проще, для него отлично подходит обычный леворекурсивный парсер без наворотов.
+13
Сорри за оффтоп, но плиз, добавьте мягкий знак в слово «находитЬся» в заголовке.
+2
Синтаксис объявления переменных в Си меня вполне устраивает, и я не вижу в нем ничего непонятного; то, что объявление совмещено с выражениями тоже очень удачно.
А вот для функций я бы предпочел ключевое слово func вначале, а возвращаемый тип после агрументов
принцип очень простой: сначала пишем существующее, затем новое. То есть сначала пишем имя сущности, которая нам известна (ключевое слово или имя типа), а затем вводим новый идентификатор. Лично мне так гораздо понятнее.
Ключевые слова var и let, повсеместно используемые в новых языках (и не только в Go) для объявления переменных, очень удобны для компилятора. Они снимают любые неоднозачности, связанные с разбором: после них может быть только объявление переменных.
Удобны ли они для человека? Думаю, кому как, мне не очень. Но это дело привычки.
А вот объявление функций с ключевого слова было бы действительно удобно — по общему принципу с объявлением структур, классов, перечислений и т.д. Решалась бы путаница с указателями на функции. Упростилась бы работа компиляторов и IDE. Искать объявления функций в коде стало бы легче. Упростилась бы реализация объявления вложенных функций (напомню, еще в Паскале они были, а в современном С++ есть только частный случай в виде лямбд). Появились бы интересные дополнительные возможности: введение имен возвращаемых значений, введение специальных ключевых слов для специальных функций, удобный синтаксис для возврата сразу нескольких возвращаемых значений и т.д.
А вот для функций я бы предпочел ключевое слово func вначале, а возвращаемый тип после агрументов
func foo(int x) int
принцип очень простой: сначала пишем существующее, затем новое. То есть сначала пишем имя сущности, которая нам известна (ключевое слово или имя типа), а затем вводим новый идентификатор. Лично мне так гораздо понятнее.
Ключевые слова var и let, повсеместно используемые в новых языках (и не только в Go) для объявления переменных, очень удобны для компилятора. Они снимают любые неоднозачности, связанные с разбором: после них может быть только объявление переменных.
Удобны ли они для человека? Думаю, кому как, мне не очень. Но это дело привычки.
А вот объявление функций с ключевого слова было бы действительно удобно — по общему принципу с объявлением структур, классов, перечислений и т.д. Решалась бы путаница с указателями на функции. Упростилась бы работа компиляторов и IDE. Искать объявления функций в коде стало бы легче. Упростилась бы реализация объявления вложенных функций (напомню, еще в Паскале они были, а в современном С++ есть только частный случай в виде лямбд). Появились бы интересные дополнительные возможности: введение имен возвращаемых значений, введение специальных ключевых слов для специальных функций, удобный синтаксис для возврата сразу нескольких возвращаемых значений и т.д.
+6
собственно в языке Rust приблизительно так и сделано:
doc.rust-lang.org/book/functions.html
doc.rust-lang.org/book/functions.html
0
Видать комитету тоже так удобнее:
;-)
Для особых ценителей можно:
auto (*cb1)(int) -> int;
auto proc(int x) -> int
{
return 31337;
}
;-)
Для особых ценителей можно:
#define func auto
func proc(int x) -> int;
+1
О нет. Это сделано нифига не для удобства. Просто в шаблонных функциях так бывает, что тип результата зависит от типа параметров — и тогда его описать до имени функции никак не получится!
А так да — можно использовать вполне и без шаблонов.
А так да — можно использовать вполне и без шаблонов.
+1
Да как бы да. Я прочитал свой пост, пока думал как лучше переписать — время вышло. Махнул рукой — кому нужно, тот поймёт :)
0
Удобство использования стало как бы бонусом, описывать указатели на функции возвращающие функции стало удобнее
auto (*func_ptr)(int) ->
auto (*)(float, int) ->
int (*)()
0
Это проблема исключительно парсера С++. В C# прекрасно работает так:
То есть T используется еще до указания, что тип-параметр.
IEnumerable<T> Where<T>(Funct<T,bool> predicate)
То есть T используется еще до указания, что тип-параметр.
0
Не тот случай, в С++ выражение по типу
Тоже будут работать без всякого нового объявления. Новый тип объявления нужен в случае когда шаблонный тип один а возвращается совершенной другой. Например.
Можно конечно извернутся и слепить нечто такое
но это не совсем красиво, да и не уверен что будет работать везде и всегда.
Ну и да, не стоит забывать что в C# дженерики, а не шаблоны, они работают несколько иначе.
template<class T>
IEnumerable<T> Where(Funct<T,bool> predicate)
Тоже будут работать без всякого нового объявления. Новый тип объявления нужен в случае когда шаблонный тип один а возвращается совершенной другой. Например.
struct A{};
struct B
{
A func();
};
template<class T>
auto Func(T& _val) -> decltype(_val.func());
Можно конечно извернутся и слепить нечто такое
template<class T>
decltype(((T*)(0))->func()) Func(T& _val)
но это не совсем красиво, да и не уверен что будет работать везде и всегда.
Ну и да, не стоит забывать что в C# дженерики, а не шаблоны, они работают несколько иначе.
0
Ничего что template указывается до любого объявления? Причем это сделано специально чтобы помочь парсеру. Не забывайте, что С++ использует LL парсер. А LL парсер хорошо работает когда смысл написанного правее зависит от того что написано левее. Поэтому и типы слева, и template писать надо. Можно было бы отказаться, но это бы усложнио парсинг и, скорее всего, увеличило бы время компиляции.
Это к вопросу парсинга не имеет никакого отношения от слова вообще.
Ну и да, не стоит забывать что в C# дженерики, а не шаблоны, они работают несколько иначе.
Это к вопросу парсинга не имеет никакого отношения от слова вообще.
0
Это к вопросу парсинга не имеет никакого отношения от слова вообще.имеет причём довольно-таки прямое. Так как у нас тип в дженериках предназначен для всяко-разных проверок и, в общем, не вляет на генерируемый код, то кроме типов в угловых скобках ничего указать нельзя. В C++ — можно, откуда и все беды.
+2
Парсинг строит синтаксическое дерево, ему по большому счету без разницы как потом это дерево обрабатывается. Вы вообще не о том говорите.
-2
Насколько я понял, речь о том, что парсить вот такое в качестве возвращаемого типа в C++ — норма:
decltype(decltype(_val.func())::n + 10)::result_type
, а в C# — нет+2
Нужно просто уметь парсить нечто зависящее от типа — а для этого нужно уметь понимать где у нас типы, а где — нетипы.
Я считал что этот пример всем, кто берётся рассуждать о тонкостях C++ известен.
Он просто очень выпукло показывают проблему во всей красе: в зависимости от опций компилятора у вас может быть по-разному построено синтаксическое дерево! Не выбраться другая функция и по другому посчитаться константа, нет — по разному будет именно всё разобрано. Без всяких ifdef'ов или define'ов (они-то вообще до компилятора отрабатывают и как бы «не в счёт»).
Я считал что этот пример всем, кто берётся рассуждать о тонкостях C++ известен.
Он просто очень выпукло показывают проблему во всей красе: в зависимости от опций компилятора у вас может быть по-разному построено синтаксическое дерево! Не выбраться другая функция и по другому посчитаться константа, нет — по разному будет именно всё разобрано. Без всяких ifdef'ов или define'ов (они-то вообще до компилятора отрабатывают и как бы «не в счёт»).
+3
Это вы не о том говорите. Возьмите всем известный пример:
В C# подобное невозможно потому что дженерики параметризуются только типами.
int x = confusing<sizeof(x)>::q < 3 > (2);
Так вот в зависимости от того явзяется у вас q
типом или переменной у вас будет построено разное синтаксическое дерево. Хабрапарсер выбирает один вариант (тот, который ему больше нравится), но там есть ещё и второй, где вначале считается confusing<sizeof(x)>::q < 3
и вот уже это сравнивается с двойкой. В C# подобное невозможно потому что дженерики параметризуются только типами.
0
Та же самое может быть в C#. Вместо имени типа может оказаться переменная и выражение
IEnumerable может быть воспрнято как (IEnumerable < T ) > что-то там, где IEnumerable и T — переменные. Но у C# грамматика более стройна и не допускает таких ошибок, после имени типа выражение не напишешь, нужно тип в скобки брать, что делает парсинг однозначным,
Например если для C++ запретить приведения типов в операторной форме, то подобной проблемы не возникнет. Да и многих других проблем можно избежать если поправить синтаксис, но из-за совместимости этого не делают.
IEnumerable может быть воспрнято как (IEnumerable < T ) > что-то там, где IEnumerable и T — переменные. Но у C# грамматика более стройна и не допускает таких ошибок, после имени типа выражение не напишешь, нужно тип в скобки брать, что делает парсинг однозначным,
Например если для C++ запретить приведения типов в операторной форме, то подобной проблемы не возникнет. Да и многих других проблем можно избежать если поправить синтаксис, но из-за совместимости этого не делают.
0
Кому трудно привыкнуть писать в GO var, может использовать оператор :=
0
еще раз… в си тип размазан по определению, за исключением простых типов: в массиве — тип и размерность, в функции — возвращаемого и аргументов, указатели — привязанно к идентификатору, а не типу… а модификаторы… кто во что горазд.
0
В случае с Go некоторые примеры кода выглядят так, будто их скопировали из описания Компонентного Паскаля.
Да и зачем, спрашивается, тащить в новый язык полувековые дефекты и костыли, если можно взять что-то более продуманное, удобное и эффективное?
Да и зачем, спрашивается, тащить в новый язык полувековые дефекты и костыли, если можно взять что-то более продуманное, удобное и эффективное?
-6
Ну я бы не сказал что это полувековые костыли:) Да и кто сказал что в компонентном паскале костыли?
Костыли проявляются после более глубокого изучения языка, а подход типа «раз похоже на компонентный паскаль — значит костыли» совершенно неправильный.
Вот например кто нибудь знает, что в С/С++ (и также в C#/Java) есть дефект с приоритетом операций? Сможете назвать и обосновать?
Костыли проявляются после более глубокого изучения языка, а подход типа «раз похоже на компонентный паскаль — значит костыли» совершенно неправильный.
Вот например кто нибудь знает, что в С/С++ (и также в C#/Java) есть дефект с приоритетом операций? Сможете назвать и обосновать?
+4
Костыли — это про сишный синтасис, причём судя по некоторым статьям — число дефектов в том же С++ год от года только растёт.
0
& и |?
<<?
<<?
0
Операторы сравнения < <= > >= == != имеет приоритет выше чем битовые операции & | ^
В результате например вот такая вполне логичная конструкция
без скобок вокруг «x & 0x07» некорректна.
В результате например вот такая вполне логичная конструкция
if(x & 0x07 > 4)
без скобок вокруг «x & 0x07» некорректна.
0
f func(func(int,int) int, int) int
Вот зачем было в go изобретать велосипед, если уже давно в ML-образных языках используется синтаксис вроде
f : ((int, int) -> int, int) -> int
ИМХО, если хотелось сделать как привычнее, надо было брать синтаксис C. А тут хотели как лучше, явно посмотрели в сторону функциональных языков (да и не только их, любая статья по теории типов пестрит подобной нотацией), но почему-то не захотели вводить двоеточие и стрелочку.
+6
А зачем двоеточия и стрелочки, кроме как для красоты?
+1
Принято так, вроде
Да и, имхо, нагляднее, чем func. Хотя, может, и дело привычки.
Да и, имхо, нагляднее, чем func. Хотя, может, и дело привычки.
+1
А, ну в ML-языках двоеточие потому, что запись через пробел (
f x
) — это применение функции (f(x)
)0
Перепишем вашу сложную функцию на Go
Даже если взять более сложную функцию из той же статьи.
Так что дело не в бобине.
f func(func(int,int) int, int) int
с указанием типа слеваint f func(int func(int,int), int)
и увидим, что и так нет сложностей с прочтением.Даже если взять более сложную функцию из той же статьи.
f func(func(int,int) int, int) func(int, int) int
(int func(int, int)) f func(int func(int,int), int)
Так что дело не в бобине.
+2
int f func(int func(int,int), int)Есть проблема с прочтением. В выделенном мной месте непонятно, что следует за int. Анализатору надо заглянуть вперёд, понять, что там func и только потом понять, что это аргумент-функция, а не аргумент-число. Так-то.
+1
Вот такие имеет отличия определение переменных в языках семейства C и Go. Очевидно, Go явно в этом выигрывает. Но если теперь вспомнить, какие языки выросли из старого доброго С – это С++, C#, Java — все они используют определение переменных такого типа.
Очень «тонкий» намек на преимущества Go, по сути бред.
Сложность типов в C вызвана тремя компонентами:
1) Указателями и повсеместным их использованием
2) Отсутствием нормального описания функционального типа
3) const
В C# и Java всего этого нет, поэтому и проблем с описанием типов нет. В С++ только const иногда мешает, да и то не часто. Так что никакого разительного преимущества Go перед современными языками нету.
Кстати писать тип после придумали в ML, лет за 40 до изобретения Go. Там даже еще дальше пошли — аннотации типов применяются не только к объявлениям, но и в выражениями. Это в сочетании с автоматическим выводом типов добавляет удобства в разы.
+7
Очевидно, Go явно в этом выигрывает. Но если теперь вспомнить, какие языки выросли из старого доброго С – это С++, C#, Java — все они используют определение переменных такого типа. И они построены на парадигмах ООП и не используют (или практически не используют) передачу указателей на функции, все это нам заменили классы. Недостатки, которые выявляются у определения типа переменной слева, улетучиваются при использовании ООП.
Вы, вырвали слова из контекста и все сказано в следующих 2-х предложения.
+2
К слову, в GO указатели используются повсеместно. И проблем с типами нет :)
+1
Написание типа после имени — это необходимость, которую осознали слишком поздно. Такой подход, к примеру, позволяет компилятору выводить тип возвращаемого значения из типа аргументов функции. В C++11 даже (в очередной раз) ввели специальный синтаксис для этого:
Написать
Кроме того, как уже упоминалось, такой подход существенно упрощает как компилятор, так и разработку инструментов. Вспомним ключевое слово
Если бы было ключевое слово для объявления переменных, такой проблемы бы не возникло:
Языку 30 лет, а мой текстовый редактор, к примеру, зачастую не может распознать объявления переменных. Если бы было слово var, риск ошибки был бы практически нулевой.
Ну и мне лично кажется, что подход с объявлением типа после имени делает опциональный вывод типов более логичным.
template <class U, class V>
auto add(U const& u, V const& v) -> decltype(u + v) {
return u + v;
}
Написать
decltype(u+v)
вместо auto
нет возможности — там компилятору ещё не видны имена (и соответствующие типы) u и v.Кроме того, как уже упоминалось, такой подход существенно упрощает как компилятор, так и разработку инструментов. Вспомним ключевое слово
typename
из C++:template <class Iterator>
void doSomething(Iterator it) {
// Тут необходимо слово typename, чтобы компилятор мог понять, что вы хотите:
// 1) объявить переменную v с типом указателя на Iterator::value_type;
// 2) вызвать Itarator::value_type.operator*(v), где v нужно взять из окружающего контекста.
typename Iterator::value_type * v;
}
Если бы было ключевое слово для объявления переменных, такой проблемы бы не возникло:
var v: *Iterator::value_type; // объявление переменной
Iterator::value_type * v; // умножение
Языку 30 лет, а мой текстовый редактор, к примеру, зачастую не может распознать объявления переменных. Если бы было слово var, риск ошибки был бы практически нулевой.
Ну и мне лично кажется, что подход с объявлением типа после имени делает опциональный вывод типов более логичным.
// Если тип опустить, то компилятор его выводит, логично.
// К тому же, имена всегда выровнены по левому краю.
var x = 5;
var y: int = 5;
var z: MyType = init();
// Хм... Ок...
auto x = 5;
int y = 5;
MyType z = init();
+8
Точно, тот самый знаменитый пример:)
X * Y; // что это - умножение или объявление указателя?
+3
UFO just landed and posted this here
int (*(*fp)(int (*)(int, int), int))(int, int)
пара typedef'ов обычно решает проблему нечитаемости
-1
Пара typedef'ов обычно решает проблему нечитаемостиАга, конечно. Особенно если выражение встречается не в коде, а в документации. Пример с сигналом — он же не из воздуха взялся, а из официальной документации.
Хорошо хоть названия параметров сохранились! По синтаксису они там не нужны, но выкидывание
fp
превратит выражение в паззл:void (*signal(int, void (*)(int)))(int);
Вообще же — писать можно на чём угодно, хоть на брайнфаке, но то, что у вас выражение в полстроки невозможно понять и требуется сложный анализ производить — это же ненормально…Но вообще ворос: справа или слева не очень приниципиален. Можно слева (Java), можно справа (Go), главное — не со всех сторон сразу (как в C/C++). Описания переменных — это одно из мест в C/C++, которые сделаны очевидно плохо.
+2
Спасибо за статью. Ещё, когда тип пишется справа, для меня это удачно укладывается в математическое представление типа как множества принадлежащих ему объектов.
var x int // x принадлежит множеству целых чисел
var p *int // p принадлежит множеству указателей на объекты, принадлежащие множеству целых чисел
var a [3]int // a принадлежит множеству трёхэлементных массивов объектов, принадлежащих множеству целых чисел
+1
Где находиться типу: справа или слева?Нигде. Типы должны выводиться автоматически. Если тип всё же нужно указать явно, то он должен быть указан для всего выражения.
-5
Это очень субъективно. Мне например намного привычнее Сишный способ, первое время я вообще не понимал что там за мешанина в Go-коде.
Понятие удобства в данном случае очень сильно зависит от того, какой у человека бэкграунд. Единственный объективный способ сравнения — это посадить 100 программистов, которые никогда не писали на языках со строгой типизацией, и замерить сколько времени их мозг тратит на разбор си-образного и го-образного способов объявления типов. Все остальное — субъективщина и холиворы.
Понятие удобства в данном случае очень сильно зависит от того, какой у человека бэкграунд. Единственный объективный способ сравнения — это посадить 100 программистов, которые никогда не писали на языках со строгой типизацией, и замерить сколько времени их мозг тратит на разбор си-образного и го-образного способов объявления типов. Все остальное — субъективщина и холиворы.
+1
Sign up to leave a comment.
Где находиться типу: справа или слева?