Pull to refresh

Comments 36

Уже после нажатия «опубликовать» увидел кое-какие ошибки — исправил.
Не все ошибки я исправил…
Но, надеюсь, главная мысль «разделяй и властвуй» от читающих не ускользнула. Иначе говоря, главная идея была в том, чтобы используя пользовательские типы сделать из однострочных монстров читаемые определения.
Возможно, неудачно я выбрал примеры (да еще и в спешке налажал) — надо было именно об указателях на функции, как наиболее «страшные» писать.
Ну что ж, век живи — век учись, возможно, следующий блин будет не комом.
Во-первых, вы ошиблись:
typedef const const_int_ptr* const_ptr; // неизменяемый указатель на предыдущий указатель
Это изменяемый указатель на неизменяемый указатель на неизменяемый int.

Во-вторых, в корне не согласен. Вы добавили ненужный слой абстракции над нотацией типа переменной. Каждому программисту, который прочитает ваш код, придётся распарсивать вашу собственную нотацию вида const_ptr или const_int_ptr. Заметьте, что const и ptr у вас в обоих случаях значат совершенно разные вещи: в первом случае у вас константный указатель на указатель, а во втором случае — указатель на константный int.

Если не брать объявления указателей на функции (их, как раз, лучше выносить в typedef), Си имеет прекрасную нотацию. Главное её использовать правильно; например, писать const справа:

int const * const * const ptr;

Читайте справа налево: ptr — это константный указатель на константный указатель на константный int.

Ничего сложного.
Ваша статья не имеет смысла, потому что вы ошиблись в самом начале:

const int *var; — указатель на переменную, которая не может менять значение.
int const *var; — указатель, который не может менять свое значение, на переменную int, которая может…
— эти 2 объявления означают абсолютно одно и то же. Чтобы получить неконстантный указатель на константные данные, нужно написать:

int * const z;

Все на самом деле логично: то, что находится до символа *, относится к тому на что указываем, а то что после * — к самому указателю. Демо
Ох, вы и меня запутали. z в моём комментарии — это конечно же константный указатель на неконстантные данные.
К чему я призываю? Да вот к чему: коллеги-программисты и те, кто хочет ими стать! Будьте проще, не кичитесь своими знаниями глубин Си


Просто не используйте костыль «const» всуе а только там, где он действительно необходим.
А я бы сказал так. Убирайте «const» только тогда, когда это необходимо. А вообще суйте его везде, где только можно.
Как сказал когда-то мой друг: «Лучше переконстить, чем недоконстить» :)
Полюбить очень просто. Есть такой эффект — «Стокгольмский синдром» называется. Очень помогает в подобных случаях.
Вот согласен на 100%. После некоторого времени с Си привыкаешь к указателям и прочим радостям. Потом берешь язык уровня повыше (С++ не берём, он другой и инопланетный) — и тут-то и понимаешь как тебе не хватает тех примитивных конструкций, которые можно было сложить в три этажа и обернуть скобками для лучшей читаемости, а не для компилятора…
Очень сомневаюсь, что нечто трехэтажное обернутое в скобки улучшит читаемость.
Я верно понял, что вся статья медленно подводила нас к большой разнице между двумя очень похожими утверждениями, которые на проверку оказались идентичными?
Тогда о чем статья?
Не совсем так. Посыл статьи правильный — синтаксис упрощать надо, как сделано с указателями, например, в С++. Вот только автор сначала запутался в том, что он упрощать собрался, а потом результат его упрощений оказался крайне сомнительным, да и, опять же, с ошибками.
Насчёт С++ — я имею ввиду, что в С++ можно выражать типы с указателями каким-то таким образом:
ptr<const ptr<volatile ptr<const int> > >
Что в сложных случаях будет читабельнее, чем Си вариант. Или с привкусом D:
ptr ! const ptr ! volatile ptr ! const int
В C++ вообще не нужны многоэтажные объявления указателей — там есть инкапсуляция. Если проще —
const std::string& var
вместо
const char* const* var
— char* скрыт в объекте std::string. Да еще и ссылка вместо указателя.
Это-то всё фигня. Самое сложное — это функции, и там будет либо многоэтажные объявления, либо куча typedef-ов.
Откройте для себя std::function и не будет вам никаких многоэтажных объявлений с кучей тайпдефов.
Тот же самый typedef, вид сбоку.
Как Вы в таком случае себе представляете сферическое объявление указателя на функцию в вакууме?
Приведите пример сложного и непонятного объявления std::function
Даже в сложных случаях типа
std::function< std::function<int (int)> (std::function<int (int, int)>) >

— «функция, принимающая функцию, принимающую два аргумента типа int и возвращающую int, возвращающая функцию, принимающую int и возвращающую int» — все понятно, читабельно и логично. А как будет выглядеть это на Сишных указателях? Мне например и пробовать сейчас написать это страшно.
«сферическое объявление указателя на функцию в вакууме»:
void(*(*(*(*f())(void(*)()))(void(*)()))(void(*)()))();
Разбавить квалификаторами по вкусу.

Выделим типы:
typedef void(*g0)();
typedef g0(*g1)(g0);
typedef g1(*g2)(g0);
typedef g2(*g3)(g0);

g3 g();
Теперь читаемо.

Если пофантазировать и сказать, что у нас весь код написан с использованием std::function, то код выглядит вот так:
std::function<
	std::function<
		std::function<
			std::function<
				void()
			>(std::function<void()>)
		>(std::function<void()>)
	>(std::function<void()>)
> h();
Я бы сказал, что никаких плюсов перед typedef в плане выразительности std::function не представляет. Наоборот, я мало того, что указываю, что тип — функция, я ещё и указываю сигнатуру функции рядом используя Си нотацию. Многословненько.
В вашем примере для читающего код сигнатуры скрыты за кучей typedef, и без их разбора ему вообще не понятно, что таки туда передавать. Если std::function кажется слишком длинно, можете с помощью using хоть fn ее называть, и будет
fn< fn<int (int)>  ( fn<int (int, int)> ) >
Дело не в том, что длинно. Дело в том, что используя std::function я пишу то, что я бы написал, используя typedef или инлайн-нотацию, ПЛЮС описываю непосредственно шаблон.
Как я выше сказал, без описания сигнатуры функции это вообще не нужно. А в данном случае читающий сразу понимает, что куда передается и возвращается. Без хождения по тайпдефам и запоминания каждого, и без парсинга скобок и звездочек.
Видимо, вы просто фанат std::function. :) На мой взгляд, вариант с std::function, в котором почти в два раза больше скобочек и ровно в два раза больше упоминаний функций, выглядит достаточно уродливо. Я бы использовал typedef.
Ещё лучше использовать using:

using func_t = int(int*);
using func2_t = float(func1_t);
Стоит признать, что std::function это не равнозначная замена, у нее есть рантайм издержки.
Она не хранит в себе ничего кроме указателя, и издержки такие же. А clang их даже инлайнить умеет.
Она type-erased. Хранит указатель на базовый класс с виртуальным operator(). Поэтому один лишний косвенный вызов нужен. (Это сделано, чтобы можно было хранить любые callable: помимо указателей на функции, это может быть например указатель на метод или функциональный объект)
Современные компиляторы убирают использование RTTI, если на этапе компиляции можно вычислить его результат (гарантия конечно не 100%, но в тривиальных случаях да). Clang догадывается даже заинлайнить std::function, проверено.
Я знаю. Но всегда гарантировать это невозможно.
И получаем аллокацию памяти на ровном месте, где она, быть может, не нужна ещё.
Это пример, а не руководство к полной замене во всех случаях. Во всех случаях при помощи структур и объектов можно избежать использования конструкций «указатель на указатель на...»
Sign up to leave a comment.

Articles