Pull to refresh

Comments 56

Для C++ еще есть известный пазл: константный указатель/указатель на константу
int *const p1
int const* p2
const int* p3
Существует простое правило:
const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке. В этом случае, очевидно, модифицирует то, что прямо после.
И сразу легко понять что
 const int ** const p4

это константный указатель на указатель на константный int.
Существует простое правило:
const модифицирует то, что написано прямо перед ним, за исключением (какой С++ без исключений) случая, когда это первое слово в строке.

Хаха, это простое правило в стиле C++, а в стиле C правило звучит так «надо прочитать объявление в обратную сторону».

const int *const p; // p - это константный указатель на int костантный
const int *const *p; // p - это указатель на константный указатель на int костантный
const int **const **p; // p - это указатель на указатель на константный указатель на указатель на int константный
Тот же самый самый костыль в другой форме
const int p; //int константный
int const * //указатель на константный int

Единообразия нет все равно.
«Правило в стиле С++» ничуть не сложнее, что модифицирует первый const знают все.
ЕМНИП синтаксис объявлений в C был сделан так, как удобнее разбирать компилятору. В Паскале же наоборот, как удобно читать человеку, за счет усложнения компилятора. В Go, видимо, нашли некий компромисс. :)
Как раз наоборот: когда компилятор C++ строит AST и встречает int name..., он не знаете что будет дальше — объявление переменной или функции, поэтому эту информацию надо или запоминать где-то или возвращаться назад по исходнику. К тому же для языков с подобными грамматиками сложнее программировать восстановление после сбоев во время синтаксического анализа. Для паскаля как раз проще, для него отлично подходит обычный леворекурсивный парсер без наворотов.
Сорри за оффтоп, но плиз, добавьте мягкий знак в слово «находитЬся» в заголовке.
Синтаксис объявления переменных в Си меня вполне устраивает, и я не вижу в нем ничего непонятного; то, что объявление совмещено с выражениями тоже очень удачно.
А вот для функций я бы предпочел ключевое слово func вначале, а возвращаемый тип после агрументов
func foo(int x) int 

принцип очень простой: сначала пишем существующее, затем новое. То есть сначала пишем имя сущности, которая нам известна (ключевое слово или имя типа), а затем вводим новый идентификатор. Лично мне так гораздо понятнее.
Ключевые слова var и let, повсеместно используемые в новых языках (и не только в Go) для объявления переменных, очень удобны для компилятора. Они снимают любые неоднозачности, связанные с разбором: после них может быть только объявление переменных.
Удобны ли они для человека? Думаю, кому как, мне не очень. Но это дело привычки.
А вот объявление функций с ключевого слова было бы действительно удобно — по общему принципу с объявлением структур, классов, перечислений и т.д. Решалась бы путаница с указателями на функции. Упростилась бы работа компиляторов и IDE. Искать объявления функций в коде стало бы легче. Упростилась бы реализация объявления вложенных функций (напомню, еще в Паскале они были, а в современном С++ есть только частный случай в виде лямбд). Появились бы интересные дополнительные возможности: введение имен возвращаемых значений, введение специальных ключевых слов для специальных функций, удобный синтаксис для возврата сразу нескольких возвращаемых значений и т.д.
В языке Rust сделано почти так же как в Go: типы после имен переменных, ключевые слова let и let mut (вместо var), fn (вместо func) для фукнций.
Видать комитету тоже так удобнее:
auto (*cb1)(int) -> int;

auto proc(int x) -> int
{
  return 31337;
}

;-)

Для особых ценителей можно:
#define func auto

func proc(int x) -> int;
О нет. Это сделано нифига не для удобства. Просто в шаблонных функциях так бывает, что тип результата зависит от типа параметров — и тогда его описать до имени функции никак не получится!

А так да — можно использовать вполне и без шаблонов.
Да как бы да. Я прочитал свой пост, пока думал как лучше переписать — время вышло. Махнул рукой — кому нужно, тот поймёт :)
Удобство использования стало как бы бонусом, описывать указатели на функции возвращающие функции стало удобнее
auto (*func_ptr)(int) -> 
    auto (*)(float, int) ->
        int (*)()

Это проблема исключительно парсера С++. В C# прекрасно работает так:
IEnumerable<T> Where<T>(Funct<T,bool> predicate)

То есть T используется еще до указания, что тип-параметр.
Не тот случай, в С++ выражение по типу
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# дженерики, а не шаблоны, они работают несколько иначе.
Ничего что template указывается до любого объявления? Причем это сделано специально чтобы помочь парсеру. Не забывайте, что С++ использует LL парсер. А LL парсер хорошо работает когда смысл написанного правее зависит от того что написано левее. Поэтому и типы слева, и template писать надо. Можно было бы отказаться, но это бы усложнио парсинг и, скорее всего, увеличило бы время компиляции.

Ну и да, не стоит забывать что в C# дженерики, а не шаблоны, они работают несколько иначе.

Это к вопросу парсинга не имеет никакого отношения от слова вообще.
Это к вопросу парсинга не имеет никакого отношения от слова вообще.
имеет причём довольно-таки прямое. Так как у нас тип в дженериках предназначен для всяко-разных проверок и, в общем, не вляет на генерируемый код, то кроме типов в угловых скобках ничего указать нельзя. В C++ — можно, откуда и все беды.
Парсинг строит синтаксическое дерево, ему по большому счету без разницы как потом это дерево обрабатывается. Вы вообще не о том говорите.
Насколько я понял, речь о том, что парсить вот такое в качестве возвращаемого типа в C++ — норма: decltype(decltype(_val.func())::n + 10)::result_type, а в C# — нет
Нужно просто уметь парсить нечто зависящее от типа — а для этого нужно уметь понимать где у нас типы, а где — нетипы.

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

Он просто очень выпукло показывают проблему во всей красе: в зависимости от опций компилятора у вас может быть по-разному построено синтаксическое дерево! Не выбраться другая функция и по другому посчитаться константа, нет — по разному будет именно всё разобрано. Без всяких ifdef'ов или define'ов (они-то вообще до компилятора отрабатывают и как бы «не в счёт»).
Это вы не о том говорите. Возьмите всем известный пример:
int x = confusing<sizeof(x)>::q < 3 > (2);
Так вот в зависимости от того явзяется у вас q типом или переменной у вас будет построено разное синтаксическое дерево. Хабрапарсер выбирает один вариант (тот, который ему больше нравится), но там есть ещё и второй, где вначале считается confusing<sizeof(x)>::q < 3 и вот уже это сравнивается с двойкой.

В C# подобное невозможно потому что дженерики параметризуются только типами.
Та же самое может быть в C#. Вместо имени типа может оказаться переменная и выражение
IEnumerable может быть воспрнято как (IEnumerable < T ) > что-то там, где IEnumerable и T — переменные. Но у C# грамматика более стройна и не допускает таких ошибок, после имени типа выражение не напишешь, нужно тип в скобки брать, что делает парсинг однозначным,

Например если для C++ запретить приведения типов в операторной форме, то подобной проблемы не возникнет. Да и многих других проблем можно избежать если поправить синтаксис, но из-за совместимости этого не делают.
Кому трудно привыкнуть писать в GO var, может использовать оператор :=
еще раз… в си тип размазан по определению, за исключением простых типов: в массиве — тип и размерность, в функции — возвращаемого и аргументов, указатели — привязанно к идентификатору, а не типу… а модификаторы… кто во что горазд.
В случае с Go некоторые примеры кода выглядят так, будто их скопировали из описания Компонентного Паскаля.
Да и зачем, спрашивается, тащить в новый язык полувековые дефекты и костыли, если можно взять что-то более продуманное, удобное и эффективное?
Ну я бы не сказал что это полувековые костыли:) Да и кто сказал что в компонентном паскале костыли?
Костыли проявляются после более глубокого изучения языка, а подход типа «раз похоже на компонентный паскаль — значит костыли» совершенно неправильный.
Вот например кто нибудь знает, что в С/С++ (и также в C#/Java) есть дефект с приоритетом операций? Сможете назвать и обосновать?
Костыли — это про сишный синтасис, причём судя по некоторым статьям — число дефектов в том же С++ год от года только растёт.
Операторы сравнения < <= > >= == != имеет приоритет выше чем битовые операции & | ^
В результате например вот такая вполне логичная конструкция
if(x & 0x07 > 4)

без скобок вокруг «x & 0x07» некорректна.
Чего это она некорректна? Очень даже она корректна. Это битовое умножение x и 1, то есть 0, если x=0 и 1 в противном случае.
Ну в смысле корректна, но бестолкова. Для действий с bool есть логические операции && и ||, которые совершенно правильно имеют приоритет ниже чем сравнения.
f func(func(int,int) int, int) int

Вот зачем было в go изобретать велосипед, если уже давно в ML-образных языках используется синтаксис вроде
f : ((int, int) -> int, int) -> int

ИМХО, если хотелось сделать как привычнее, надо было брать синтаксис C. А тут хотели как лучше, явно посмотрели в сторону функциональных языков (да и не только их, любая статья по теории типов пестрит подобной нотацией), но почему-то не захотели вводить двоеточие и стрелочку.
А зачем двоеточия и стрелочки, кроме как для красоты?
А, ну в ML-языках двоеточие потому, что запись через пробел (f x) — это применение функции (f(x))
Перепишем вашу сложную функцию на 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) 


Так что дело не в бобине.
int f func(int func(int,int), int)
Есть проблема с прочтением. В выделенном мной месте непонятно, что следует за int. Анализатору надо заглянуть вперёд, понять, что там func и только потом понять, что это аргумент-функция, а не аргумент-число. Так-то.
Да что вы все за анализатор то переживаете. На C++ пережевывает и не потеет. Читать люди будут. И у людей есть вполне очевидные проблемы с чтением сложных конструкций в C++.
А в Go что слева ставь тип, что справа – понять можно без проблем, а дальше уже вопрос вкуса и привычки.
Вот такие имеет отличия определение переменных в языках семейства C и Go. Очевидно, Go явно в этом выигрывает. Но если теперь вспомнить, какие языки выросли из старого доброго С – это С++, C#, Java — все они используют определение переменных такого типа.

Очень «тонкий» намек на преимущества Go, по сути бред.
Сложность типов в C вызвана тремя компонентами:
1) Указателями и повсеместным их использованием
2) Отсутствием нормального описания функционального типа
3) const

В C# и Java всего этого нет, поэтому и проблем с описанием типов нет. В С++ только const иногда мешает, да и то не часто. Так что никакого разительного преимущества Go перед современными языками нету.

Кстати писать тип после придумали в ML, лет за 40 до изобретения Go. Там даже еще дальше пошли — аннотации типов применяются не только к объявлениям, но и в выражениями. Это в сочетании с автоматическим выводом типов добавляет удобства в разы.
Очевидно, Go явно в этом выигрывает. Но если теперь вспомнить, какие языки выросли из старого доброго С – это С++, C#, Java — все они используют определение переменных такого типа. И они построены на парадигмах ООП и не используют (или практически не используют) передачу указателей на функции, все это нам заменили классы. Недостатки, которые выявляются у определения типа переменной слева, улетучиваются при использовании ООП.

Вы, вырвали слова из контекста и все сказано в следующих 2-х предложения.
Вы, вырвали слова из контекста и все сказано в следующих 2-х предложения.
Разве это изменило суть высказывания?

Расскажите что вы имели ввиду этой фразой.
К слову, в GO указатели используются повсеместно. И проблем с типами нет :)
Я вот нашел в интернетах эквивалентные программы на C и Go, и на Go указателей не было вообще, а на C около 20 мест с указателями.

Причина простая — в C массив это указатель, а в Go используются слайсы. Ну и в Go вывод типов есть, а в C типы указываются явно.
Написание типа после имени — это необходимость, которую осознали слишком поздно. Такой подход, к примеру, позволяет компилятору выводить тип возвращаемого значения из типа аргументов функции. В C++11 даже (в очередной раз) ввели специальный синтаксис для этого:
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();
Точно, тот самый знаменитый пример:)
X * Y; // что это - умножение или объявление указателя?

UFO just landed and posted this here
Я прекрасно понимаю, зачем и когда нужен typename. Я просто привёл самый популярный пример неоднозначности, которая возникала бы, если бы стандарт не предусматривал typename, и которой бы не было, если бы тип переменной шёл после ключевого слова и имени.
int (*(*fp)(int (*)(int, int), int))(int, int)


пара typedef'ов обычно решает проблему нечитаемости
Пара typedef'ов обычно решает проблему нечитаемости
Ага, конечно. Особенно если выражение встречается не в коде, а в документации. Пример с сигналом — он же не из воздуха взялся, а из официальной документации.

Хорошо хоть названия параметров сохранились! По синтаксису они там не нужны, но выкидывание fp превратит выражение в паззл:
void (*signal(int, void (*)(int)))(int);
Вообще же — писать можно на чём угодно, хоть на брайнфаке, но то, что у вас выражение в полстроки невозможно понять и требуется сложный анализ производить — это же ненормально…

Но вообще ворос: справа или слева не очень приниципиален. Можно слева (Java), можно справа (Go), главное — не со всех сторон сразу (как в C/C++). Описания переменных — это одно из мест в C/C++, которые сделаны очевидно плохо.
Спасибо за статью. Ещё, когда тип пишется справа, для меня это удачно укладывается в математическое представление типа как множества принадлежащих ему объектов.
var x int // x принадлежит множеству целых чисел
var p *int // p принадлежит множеству указателей на объекты, принадлежащие множеству целых чисел
var a [3]int // a принадлежит множеству трёхэлементных массивов объектов, принадлежащих множеству целых чисел

Где находиться типу: справа или слева?
Нигде. Типы должны выводиться автоматически. Если тип всё же нужно указать явно, то он должен быть указан для всего выражения.
Это очень субъективно. Мне например намного привычнее Сишный способ, первое время я вообще не понимал что там за мешанина в Go-коде.

Понятие удобства в данном случае очень сильно зависит от того, какой у человека бэкграунд. Единственный объективный способ сравнения — это посадить 100 программистов, которые никогда не писали на языках со строгой типизацией, и замерить сколько времени их мозг тратит на разбор си-образного и го-образного способов объявления типов. Все остальное — субъективщина и холиворы.
это посадить 100 программистов, которые никогда не писали на языках со строгой типизацией

Возможно, вы имели в виду: явной статической. В питоне типизация построже C будет.
Да, Вы абсолютно правы.
Sign up to leave a comment.

Articles

Change theme settings