Pull to refresh

Comments 64

Нет, спасибо. На 17 вполне замечательно живётся.

Но есть нюансы

Комитетчики как всегда в своем репертуаре… А что ж эти нюансы в Proposed колонке не учитываются? Глядишь, преимущество в количестве и понятности кода улетучится...


Теперь же мы можем переписать все его методы, не выполняющие модификаций, с передачей явного объектного параметра по значению, практически бесплатно этим получая улучшение производительности:

Хм, неужели компилятору все еще недостаточно указания constexpr и такого коротенького тела функции, чтобы он его заинлайнил и понял, что никаких указателей быть не должно? Это что ж получается, C++ обрастает все более и более монструозным синтаксисом во имя производительности, и все зря? Тривиальнейшие конструкции все еще можно оптимизировать? Или все эти конструкции, авторы каждой из которых говорят "смотрите, как стало понятно и лаконично написано" не работают, если всю эту понятность и лаконичность не обмазать десятком move(forward(auto&&))?

Хм, неужели компилятору все еще недостаточно указания constexpr и такого коротенького тела функции, чтобы он его заинлайнил и понял, что никаких указателей быть не должно? Это что ж получается, C++ обрастает все более и более монструозным синтаксисом во имя производительности, и все зря? Тривиальнейшие конструкции все еще можно оптимизировать? Или все эти конструкции, авторы каждой из которых говорят "смотрите, как стало понятно и лаконично написано" не работают, если всю эту понятность и лаконичность не обмазать десятком move(forward(auto&&))?

Одно дело — оптимизации, которые компилятор может (или не может) выполнить, другое — возможность явно описать то, что ты хочешь.

Стандарт, за редкими исключениями, не говорит о возможных оптимизациях, и они — явно не то, на что следует полагаться разработчику при написании кода.

явно не то, на что следует полагаться разработчику при написании кода.

(поперхнулся) Как же так? Чуть ли не у всех фич рефреном идет мысль, "вот, теперь оптимизатору будет где развернуться", а вы говорите, что полагаться не стоит? А зачем тогда все эти фичи? А когда вы пишите forward() или move() — это тоже явно не стоит полагаться?


Мда, до чего дошли...

Так все эти оптимизации в том или ином виде implementation specific и не могут регламентироваться стандартом. Развернуться-то может и есть где, но станет ли он это делать - вопрос отдельный.

Основная проблема всех последних нововведений в С++ - это «замыленность» взгляда разработчиков, которые эти фичи пилят.

Для людей, которые пишут код на плюсах последние лет 20, фичи могут казаться простыми и нужными, просто из-за огромного количества опыта во всех аспектах языка.

При этом совершенно забываются потребности новых разработчиков, которые смотря на развитие языка, будут делать предпочтение в сторону Rust/C, и т.д

Не думаю. Новые разработчики с удовлетворением отметят наличие новых фич и будут их игнорировать пока те не понадобятся. А в сторону Dart или Rust они, конечно, будут глядеть потому, что у С++ куча компиляторов, читай диалектов, и все разные, куча систем сборки и все разные, а у нормальных языков версия одна и cargo на всех одна и та же.

у С++ куча компиляторов, читай диалектов

Ну на самом деле все не так плохо. Новые фичи стандартов та же большая тройка может реализовывать с разной скоростью, но из уже реализованного при повседневной разработке все работает довольно одинаково, на моей памяти случаи какого-то различного поведения для вещей описанных в стандарте можно пересчитать по пальцам, да и те типа "у MSVC 2017 у такого-то класса из std такой-то конструктор не объявлен как noexcept" . Оптимизации разнятся, это бывает. Плюс фичи, (пока?) отсутствующие в стандарте, уникальные для некоего компилятора, но ими пользоваться, собственно, никто не заставляет, это твой выбор - пользоваться ими или нет.

куча систем сборки и все разные

Вы так говорите, как будто это что-то плохое. Иметь выбор всегда неплохо.

нормальных языков

"Нормальные языки" (хм) спасает только отсутствие желающих писать для них независимые компиляторы и тулчейны. Не факт что так будет вечно. Появится например поддержка тот же раста в gcc, и появятся определённые различия в поведении, тем более, что никакого формального стандартизованного описания языка же сейчас нет, а есть просто вот некая ad hoc имплементация на базе LLVM.

куча систем сборки и все разные

Вы так говорите, как будто это что-то плохое

Ну вообще-то плохое, зависимость, использующую другую систему сборки, сложнее добавить в уже существующий проект.

Ну на самом деле все не так плохо

Да нет, работаю на проекте, который собирается на gcc под линукс и на msvc. Порой всплывают очень неприятные проблемы сборки, связанные с различной трактовкой стандарта этими компиляторами или просто багом в одном из них. Приходится подстраиваться, так что это всё же скорее проблема, ну или отталкивающий фактор как минимум.

Ну я тоже "работаю на таком проекте", который собирается gcc и clang под Linux, gcc под игровые консоли (PlayStation и Nintendo), clang под макос и Android, и MSVC2019 (а несколько дней назад ещё и MSVC2017 поддерживался) под Windows, причём проект стартовал довольно давно, т.е. есть как код в стиле "C с классами", так и современный (C++17), и случаи, когда возникали проблемы, связанные с использованием различных компиляторов, можно реально посчитать по пальцам одной руки, да и те - я выше описал, какого они были характера. Так что я бы не назвал это ни проблемой, ни даже отталкивающим фактором. И ещё, если вы считаете, что нашли "баг в компиляторе", то подумайте ещё, потому что с вероятностью 99.99% баг все-таки не в компиляторе, а у вас.

Что ж, то что на проекте "на котором вы работаете" с этим вопросом всё так гладко — здорово. А на проекте, на котором работаю я, подобные проблемы возникали и имели характер, описанный мной выше.

То, из-за чего, как вы думаете, возникали эти проблемы, и из-за чего они возникали на самом деле - это могут быть две большие разницы. А то тут недавно была статья от одного любителя reinterpret_кастить один вектор к другому, а потом был бы компилятор виноват, что "сломал рабочий код, который на другом компиляторе прекрасно работал".

Это так. Я готов привести примеры:

  1. https://stackoverflow.com/questions/17430377/error-when-using-in-class-initialization-of-non-static-data-member-and-nested-cl

  2. https://coderwall.com/p/wksfza/sfinae-replacement-for-std-result_of

Оба случая приводят к ошибкам на gcc, но не на msvc. И неужели зависимость валидности таких конструкций от реализации компилятора/стандартной библиотеки — не недостаток?

Знаете, нагуглить можно что угодно, и в компиляторах бывают ошибки, и в стандартах неоднозначности (как и в любой документации). Вопрос в том, каков реальный процент таких случаев. На практике если у вас что-то работает с одним компилятором, но не работает с другим, то в 99.99% случаев проблема будет у вас в коде, а не в компиляторе или в стандарте.

Ну тут уже нечего ответить, вы попросту отказываетесь слушать. То, что вам проблема кажется незначительной или редкой, не значит, что её вовсе нет и она не составляет одного из недостатков языка. На этом предлагаю закончить.

Вы взяли общую проблему (неизбежность присутствия ошибок в любом более-менее сложном коде) и пытаетесь выдать её за частный недостаток некоего языка X. Что, в единственных компиляторах языков Y и Z не бывает ошибок? Конечно, бывают. Но вы не обращаете на это внимания (возможно, по причине отсутствия плотного знакомства с ними), а вот в языке X почему-то считаете это "недостатком". Это называется "передергивание".

нормальных языков версия одна

как будто-то что-то хорошее. И нет, не одна. Как минимум в процессе есть Rust GCC. Плюс всякие cranelift фактически тоже имплементят некоторое подмножество языка.

За Dart не скажу, но допускаю, что тоже имеются альтернативы.

как будто-то что-то хорошее

Вообще-то хорошее, не нужно ifdef-ы лепить под особенности различных компиляторов.

Есть разные OS и архитектуры, под которые всё равно придётся пилить эти же ifndef. В плюсах оно конечно некрасиво сделано, в отличие от Rust.

Увы, я даже знаю примеры. Где у тебя программист разбирается почти во всей stl библиотеке, но при этом "очевидные" вещи даже не знает. (В моём случае человек не знал адресную арифметику и как работают ссылки, разницу между T&/T&&)

Уже было подумал, что сделают extension-методы, как в C#.
Но нет, метод должен находиться в том же классе, на котором вызывается.

Придумал C#-edition этого комментария:

Уже было подумал, что сделают forward declaration, как в С++.

Но нет, тело метода должно следовать сразу за сигнатурой, вываливая бизнес-логику на читателя.

forward declaration это наследие старого C.


Попробуйте-ка отдекларировать заранее фунции в шаблонном классе.

Попробуйте-ка отдекларировать заранее фунции в шаблонном классе.

Если набор того, что можно подставить в этот шаблон, ограничен, то принципиальных проблем с этим нет.

Это очень частный случай. В другой translation unit нельзя вынести функцию в общем виде


template <typename T>
void S<T>::foo() { }

Ну конечно нельзя, ведь шаблоны "в общем виде" применяются по месту. Общий случай другого translation unit включает например бинарный .o, .so или DLL. Как применить из них шаблон? Но возможность эти шаблоны "наприменять" заранее и вынести результат в другой модуль есть. Другое дело что это не очень полезно, а иногда даже и вредно (код из другого модуля так просто без LTO не заинлайнишь), но в принципе могу себе представить ситуации, когда это может быть оправдано.

Понятно, я лишь отвечал комментатору выше, что красивого отделения деклараций от реализаций в C++ больше нет, а если хочешь почитать публичный интерфейс какого-нибудь std::unordered_map, придётся ломать глаза об реализацию.

Часто ограничиваются одним translation unit, но всё равно - сначала краткая декларация функций внутри класса, для читателей, потом уже после завершения декларации класса - inline методы для компилятора. Это пока не убили.

Таким образом, теперь мы можем переписать всю ту простыню кода, реализующую метод value у optional, гораздо более компактно, меньше чем в десяток строк

Какая же жесть. Комитет реально считает что вот в таком вот стиле люди будут писать код и считать это удобным?

Развиваю библиотеку "умных" оберток https://habr.com/ru/post/650701/ и для меня это нововведение весьма полезно. Сейчас код представляет собой большую портянку макросов (например, здесь https://gitlab.com/ssoft-scl/module/feature/-/blob/main/src/Detail/Operator.h), от которых теперь можно легко избавиться.
Не думаю, что такой стиль будет распространён в прикладных программах, а вот инструментальные библиотеки типа std, boost и др. обязательно это будут использовать.

mein gott. Почему вы решили неймспейс по-серьёзному назвать верблюжьим кейсом? Я б понял scl или sc_l там..

Так исторически сложилось. Такое соглашение о форматировании кода в большинстве проектов, в которых я участвовал. Похоже, Qt оказал влияние). Но всегда для удобства можно использовать псевдонимы для пространств имен.

namespace scl = ScL;

Все эти изменения... с одной стороны наверное они нужны и оправданы, но с другой ориентироваться в них все сложнее. Все эти тонкости шаблонного метапрограммирования, порождающие какие-то неоднозначности, требующие для разрешения очередные усложнения языка... В то же время простые вещи, такие как языковые расширения C/C++ https://gcc.gnu.org/onlinedocs/gcc-5.3.0/gcc/C-Extensions.html , в стандарт не попадают. Также я как-то давно предлагал на форуме isocpp расширить шаблоны возможностью использовать обычные фрагменты кода (не функции, а просто код в фигурных скобках) в качестве параметров и в качестве самих шаблонов (получилась бы удобная замена макросам для низкоуровневой кодогенерации), но отклонили.

Также я как-то давно предлагал на форуме isocpp расширить шаблоны возможностью использовать обычные фрагменты кода (не функции, а просто код в фигурных скобках) в качестве параметров и в качестве самих шаблонов (получилась бы удобная замена макросам для низкоуровневой кодогенерации), но отклонили.

Я тоже что-то не понял светлой идеи. Ведь есть же лямбды:

template<auto f = [](){ std::cout << "DEFAULT" << std::endl; }>
void foo()
{
  f();
}

int main()
{
  foo();
  foo<[](){ std::cout << "CUSTOM" << std::endl; }>();
}

И гибкость выше, потому что в лямбду можно передать параметры, в отличие от "просто кода в фигурных скобках", и вопросов с захватом переменных, которые используются в "просто коде в фигурных скобках" не возникает.

Лямбды это рантайм-механизм, а иногда нужно генерировать именно синтаксические деревья определенного вида для последующей компиляции. "Фрагментом кода" может быть например просто имя (последовательность символов, допустимая для идентификатора), которое где-то в шаблоне будет использовано как имя (или даже часть имени) поля, метода, вложенного класса...

расширить шаблоны возможностью использовать обычные фрагменты кода (не функции, а просто код в фигурных скобках) в качестве параметров

Что-то типа такого?


#define CUSTOM_OPERATOR(y) { if (1) y }

int main()
{
        CUSTOM_OPERATOR({ return 7;});
}

Оно уже работает. Ну если только в "параметре" нет запятой.

Хм, ну к такой идее я бы тоже отнесся мягко говоря настороженно, потому что такая генерация должна например поддерживать примерно такое, потому что "а почему бы и нет" (синтаксис условный):

template<code C>
some_inline_code {
  C else foo();
}

[...]

some_inline_code<{if (a == b) then bar()}>

Если такое поддерживать, то первичный синтаксический анализатор с глузду съедет (можно вообще придумать что-нибудь и позамороченней с непарными кавычками, фигурными скобками и т.п.), а если не поддерживать, то будет вечное нытье "а почему нельзя так, а почему нельзя эдак" и будут все равно пользоваться макросами и скриптами-кодогенераторами (и в принципе правильно).

Непарные кавычки и скобки - это уже уровень лексических макросов, т.е. существующий препроцессор С/С++. Я предлагаю только уровень синтаксических макросов, а значит - работа с законченными нодами синтаксического дерева, там непарных скобок и кавычек быть не может. Приведенный вами пример с точки зрения синтаксического анализа совершенно корректен и никаких проблем с ним не будет. Ошибка может выявиться только если передать блок несовместимый с "else", но это будет не бред на десятки строк, как сейчас при метапрограммировании на шаблонах, а вполне конкретная ошибка компиляци - такая же, как если бы вы вручную написали "else" без "if".

но это будет не бред на десятки строк, как сейчас при метапрограммировании на шаблонах

Да то же самое будет "syntax error блаблабла while instantiating template at блаблабла referring at блаблабла". Если такие шаблоны будут еще и вложенные, то будет соответственно бОльшая простыня. И от этой информации как правило есть польза - иначе в некоторых случаях могло бы быть непонятно, например, какой именно шаблон используется (или НЕ используется), например, если у вас там какие-нибудь хитро вложенные друг в друга пространства имен и в каждом собственный шаблон с одинаковым именем.

Приведенный вами пример с точки зрения синтаксического анализа совершенно корректен и никаких проблем с ним не будет.

Только если совсем не разбирать то что находится внутри фигурных скобок после some_inline_code. Сейчас шаблоны все-таки разбираются и синтаксическая корректность базовых конструкций проверяется, проверяется, что там, где ожидается тип, действительно указан тип (пусть даже typename аргумент шаблона), а где ожидается значение или переменная, там действительно указано значение (пусть даже это non-type template parameter). Нельзя просто взять и написать что-нибудь типа T else {...}.

Можно добавить ограничение, что такой шаблонный параметр — синтаксически работает как блок { }, и тогда
T else {...} — запрещено,
но вот такое — можно:
if (status == S_OK) { return true; } else T

"Фрагментом кода" может быть например просто имя (последовательность символов, допустимая для идентификатора), которое где-то в шаблоне будет использовано как имя (или даже часть имени) поля, метода, вложенного класса...

Так что нет, предлагается именно что безбашенная замена макросам, которым все равно, что и куда подставлять.

А замена блокам {} уже есть (и даже лучше чем просто замена), и это лямбды. Просто блоки {} - это честно говоря выглядит как туфта, потому что они не обеспечивают должного уровня обобщенности, так сказать. Например, чтобы в этом блоке использовать переменные из остальной части кода, нужно знать, как они называются, заботиться об отсутствии name clash, грубо говоря, писать не обобщенный код, а код под конкретный шаблон, и не дай бог в этом шаблоне потом что-то поменяется. ИМХО идея дохлая, понимаю комитетчиков (или кто там эту идею оценивал на isocpp).

Лямбды не могут глобально влиять на control flow. То есть, туда не положишь break или return, что было бы полезно для генерации веток switch. Хотя, если принять, что параметр — законченный блок { }, у него тоже break сам по себе недопустим в отрыве от контекста, куда будет подстановка.

Лямбды работают в run-time, поэтому будет оверхед.
Гипотетический пример,


template<code ERR_HANDLER>
void workWithFile(const char* filename) {
    if (auto file = fopen(filename)) {
        ....
    } else ERR_HANDLER;
}

workWithFile<{ abort(); }>("1.txt");
workWithFile<{ log.warn("can't open file"); }>("2.txt");

Лямбды работают в run-time, поэтому будет оверхед.

Лямбды при сборке с включенной оптимизацией как правило инлайнятся по месту. Поэтому кстати функции и лямбды рекомендуют передавать в другие функции как шаблонные параметры, а не в качестве обычных параметров (как std::function или там просто как указатель на функцию), если это возможно. У некоторых линтеров даже есть диагностика на это, например, у SonarQube.

Почему так длинно?

template <typename Self>
void foo(this Self&& self) { }

Почему нельзя было сразу сделать короче? Кому нужен std::forward будет использовать длинную запись.

void foo(this auto&& self) { }

Опять разбили принятие и сокращение на две итерации?

Да и вообще, this раньше нигде нельзя было использовать, кроме в теле метода класса, так почему бы не сделать его сразу вот таким специальным в его новом контексте?

void foo(this&& self) { }

Похоже, что краткая запись тоже работает. В статье есть пример с "рекурсивной лямбдой", там именно такой синтаксис.

Краткая запись тоже работает. Чем отличается explicit object parameter от других аргументов — тем, что перед ним стоит this. Больше на него кроме того, что он не может быть variadic pack'ом, ограничений практически не накладывается.

Подобные нововведения, хоть и решают некоторые проблемы - порождают другие. Этот стиль написания кода ИМХО - нечитаем. Или во всяком случае вызывает серьезные сложности в понимании. Уже много копий было сломано о том, что лучше - более короткая и емкая реализация или та которую можно нормально читать и поддерживать. И комбинация сложности восприятия кода с пачкой не самых очевидных «нюансов» где он не будет работать как вы хотите - оптимизма человеку, который будет работать с вашим кодом не добавит (как и вам, если вы возьметесь это модифицировать через пару месяцев).

Впрочем, возможно я что-то не понимаю в этой жизни и этот синтаксис на самом деле интуитивно понятен.

всё верно. Код должен быть быстро читаемым и поддерживаемым.

Я на С++ уже давно не кодю, сейчас у меня глаза ломаются при виде такого обилия <&&::{ }>;

с рекурсивными лямбдами вроде и раньше не было проблем, не?

auto f = [&](auto&& f, int n) → int {

if (n == 0) return 1;
return n * f(f, n — 1);

}

Пользоваться немножко неудобно:
f(f, 5);


Интересно, если f надо передать в метод, принимающий лямбду с одним аргументом, можно ли будет передать рекурсивную лямбду с this, или так компилятор на обманешь, у неё всё равно тип отличается от функтора с 1 аргументом, и придётся делать лямбду-обёртку https://godbolt.org/z/Khfr11vs6

Тип лямбд же в любом случае уникальный и определяемый реализацией. По сути всё, что вы можете с ней сделать — это передать её в шаблонный метод, которому всё равно что это за объект, главное, чтобы его можно было вызвать с нужными параметрами.


P.S. Сохранение лямбды в std::function это точно такой же вызов шаблонного конструктора, который выполняет type erasure.

все еще считаю что на заседаниях комитета должен быть мужик с пулеметом, чтобы эти ребята хоть немного чувствовали ответственность за то что превращают язык в тарабарщину

Я же правильно понимаю, что вариант

template <typename Self>
    void foo(this Self&&) { }

подчиняется всем правилам шаблонной функции и тело должно быть доступно в заголовочнике. Т.е. если логика дублируется, но сложнее чем простой геттер поля класса, то нужно всю эту логику выносить из cpp в h? А написания явных:

void foo(this X&);

мало отличается то использования cv- и ref-квалификаторов.

Шаблонные декларации заголовков возможны, тело функции может быть в другой единице трансляции.
https://godbolt.org/z/5Gzcxf8hz

Пардон, но нет. Вы разнесли объявление и реализацию. Вот смотрите:
main.cpp - тут нам нужен класс Foo
foo.h
foo.cpp

так вот, ну должно полностью тело функции шаблонной быть доступной в main.cpp, у нас единицы трансляции это как раз то, что компилируется, условно cpp файлы, не заголовочники. Т.е. тело шаблонной функции должно быть доступно и в main.cpp и в foo.cpp.

Т.е. если делать геттер по новому принципу, то всё тело должно быть, так или иначе, в foo.h. А по старому, там может быть только декларация, а реализация - в foo.cpp. Ну или по новому, но вида:

void foo(this Foo /*и тут &, && или их отсутствие + cv квалификаторы*/)

и тогда это ничем не отличается от старого подхода.

Если вам нужно просто дедуплицировать код для &, const&, &&, const&&, то ничего не мешает написать тело шаблонной функции в foo.cpp и в нём же явно её проинстанциировать.

template <typename Self>
void Foo::foo(this Self&& self) {
    // ...
}

template void Foo::foo<Foo&>(this Foo&);
template void Foo::foo<const Foo&>(this const Foo&);
template void Foo::foo<Foo>(this Foo&&);
template void Foo::foo<const Foo>(this const Foo&&);

А если использовать как замену CRTP, то ничего и не меняется по сравнению с CRTP.

Про CRTP пока не говорим. Но вот в случае дедупликации, мы всё равно пишем N функций, по сути, дублируются друг друга.

Upd: хотя, я, кажется, понял идею. Но выглядит всё равно так себе.

Да, примерно об этом я и думал. Но не получалось записать на C++

Sign up to leave a comment.

Articles