Pull to refresh

Comments 177

и ваши недополученные фичи

Вот от себя добавлю:

  1. almostEqual

    Плюс такие же функции, где epsilon передаётся, и ещё пачка функций про NaN (eq or nan, gt or nan etc...).

  2. И ещё хочется, чтобы были нормальные и не очень медленные делегаты в языке. Иначе приходится таскать FastDelegates, которым сто лет в обед (и куча макросов внутри), либо их более новую реинкарнацию https://www.youtube.com/watch?v=Mx_Q8LKltFs

  3. > std::string::starts_with

    а string::split не завезли...

Зато завезли std: :views: :split

А везде ли его завезли.

Подскажите пожалуйста, чем отличаются делегаты от лямбды?

Пройдите по ссылке и прочитайте. Если не любите читать — пройдите по второй ссылке, там видео, в обоих случаях этот вопрос объясняется.

Все же посмотрел материалы, спасибо. Но таки не понял, чем function так тяжел и чем пример в толке лучше.

The talk doesn't talk mich about the benefits of this library compare to std::function

По ссылке фаст-делегатов, получается какая-то лямбда с экстра шагами (статья 2005 года так то) или реализация лямбды для C++ < 11.

Вот и спрашиваю, в чем конкретно разница.

Да, лямбды с захватом не кастятся к указателю на функцию, но если вы принимаете темплейт или готовы носить std::function - пожалуйста захватывайте.

Вот и интересуюсь, чем вас не устраивают лямбды или какие медленные уже есть (no offence, правда интересно)

Лямбды правда медленные аж жесть, измеряли. С другой стороны, не всем нужна большая производительность/низкие задержки, но с третьей стороны, если это всё не нужно, то и C++ не нужно, для таких случаев есть куча медленных языков на выбор.

Лямбды правда медленные аж жесть, измеряли.

Что, правда? Кто измерял и как? Вы лямбды с std::function не путаете случайно, это же разные вещи? Сами по себе лямбды прекрасно инлайнятся и оптимизируются, а вот std::function, действительно, вещь относительно медленная, поэтому для передачи лямбд лучше всего использовать шаблоны там, где это возможно (т.е. там, где их будут сразу вызывать, а не хранить).

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

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

Ну предположим, только вывод "лямбды медленные аж жесть" отсюда никак не следует, мухи отдельно, котлеты отдельно. Вы, видимо, имеете в виду все-таки std::function. Опять же, медленные по сравнению с чем? С заинлайненными лямбдами - несомненно. С делегатами в C# - citation needed.

Подбросить вам ?

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

C++23 привносит move_only_function для решения проблемы.

https://ricomariani.medium.com/std-function-teardown-and-discussion-a4f148929809

Интересно. И правда жесть? А можно посмотреть как Вы измеряли производительность лямбд?

Посмотреть нет, но выяснилось, что std::function медленнее чем FastDelegates на несколько миллионов долларов.

Там, где лямбда будет заинлайнена, конечно разницы нет, более того, там никакой вид делагатов не нужен.

Вы все же путаете понятия. Лямбды и std::function это разные вещи. То что std::function медленнее чем некие FastDelegates, возможно, не спрою, не знаю, не сравнивал. Но лямбды тут совершенно не причем.
Лямбды в С++, это, по факту, синтаксический сахар над объектами классов с определенным методом operator()(....). А если лябда без захвата, то это фактически обычная функция на которую можно получить указатель. Что тут может быть жутко медленного? Обычные функторы С++ или просто функции (лямбда без захвата) с теми же накладными расходами, возможностями по оптимизации и производительностью.

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

А в чём, собственно, проблема получить указатель на лямбду с захватом?
https://godbolt.org/z/ezs5Thqjr

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

В каком же месте они "аж жесть" какие медленные и в сравнении с чем, позвольте узнать. Уж очень интересно.

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

Да причём тут указатель на функцию и std::function если речь шла про лямбды, которые неплохо встраиваются, о чём в этой теме только ленивый не написал.

вроде как делегат это указатель на любую функцию именованную или не-именованную(без имени), а лямбда это всегда указаель на не-именованную функцию, а может даже сама эта не-именованная функция, тут я думаю мнения разойдутся.

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

Каждая лямбда принадлежит к уникальному анонимному неназываемому типу. Т.е.

auto l1 = [] { return 0; };
auto l2 = [] { return 0; };
// std::is_same_v<decltype(l1), decltype(l2)> == false

Сама лямбда это не метод, но у неё есть член-operator(), который, как и у "обычного" класса, можно вызывать вручную (даже шаблонный):

auto l1 = [] (int a) { return a; };
std::cout << l.operator()(5); // prints 5

auto l2 = [] (auto a) { return a; };
std::cout << std::fixed << l2.template operator()<double>(5); // prints 5.000000
std::cout << std::boolalpha << l2.template operator()<bool>(5); // prints true

вот вот! а в C# есть полноценные делегаты, как полноценный тип, и поэтому никакой трехомудии с темплейтами не нужно.

Как было безобразие с темплейтами:

https://habr.com/ru/articles/770116/

так и осталось

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

Не очень понимаю (ну тут, ни из статьи по ссылке), в чём проблема. Там есть добавленный вопрос в посте: "А что std::function разрешает такой синтаксис:".

Ответ: да, разрешает. В стандарте нет гайда для вывода, так придётся написать тип руками. Впрочем, гайд можно написать и самому, тогда будет именно так как вы предлагали: DelegateType test_delegate = &test_class.Foo;

#include <functional>

struct A
{
    int foo()
    {
        return a;
    }

    int bar() const
    {
        return a;
    }

    int a = 5;
};

namespace std {
    template<typename R, typename T, typename... Args>
    function(R(T::*)(Args...)) -> function<R(T&, Args...)>;

    template<typename R, typename T, typename... Args>
    function(R(T::*)(Args...) const) -> function<R(const T&, Args...)>;
}

int main() {
    A a{ };
    const A b{ };

    std::function f1 = &A::foo;
    std::function f2 = &A::bar;  

    return std::invoke(f1, a) + std::invoke(f2, b) + std::invoke(f2, a);
}

Бонусом, достаточно умный компилятор это всё ещё и заоптимизирует до mov eax, 15.

хотелось бы вот такой синтаксис:

//вместо std::function f1 = &A::foo;
DelegateType f1 = a.foo;

//где DelegateType надо объявить как особенный тип
Delegate int DelegateType();

Объект f1 должен сохранить в себе и указатель-смещение к функции foo и объект а от которого эту функцию надо вызвать!

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

далее это мое личное мнение (хотя это все ИМХО мое), оно может быть и ошибочное,

но мне кажется это было бы намного удобнее чем возня с темплейтами,

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

и это все также поддается оптимизации при компиляции, насколько я знаю

Я всё ещё пытаюсь понять, какую проблему мы решаем. Засуньте лямбу, захватывающую ссылку на объект, в std::function. Или вообще самой этой лямбдой пользуйтесь, никакой возни с шаблонами там нет.

Засуньте лямбу, захватывающую ссылку на объект, в std::function.

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

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

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

а как же типобезопасность, например?

При желании можно форсить одинаковость параметров, тоже пишется один раз. И потом, чем, конкретно, это:

std::function<int(int, bool, std::string)> f = [&a]<typename... Args>(Args&&... args){
    return a.foo(std::forward<Args>(args)...);
};

f(1, true, "qwerqwerw");

менее безопасно чем это:

a.foo(1, true, "qwerqwerw");

?

Магии-то не бывает, ваш делегат будет, фактически, представлять из себя структурку, хранящую указатель на объект и указатель на код. Фактически, это и есть захватывающая лямбда. Хочется сахара? При желании ,можно написать функцию auto delegate(A &, &A::foo), которая будет такую лямбду создавать. Не думаю, что ради этого стоит аж целое ключевое слово вводить.

чем, конкретно, это:

f =

менее безопасно чем это

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

Не думаю, что ради этого стоит аж целое ключевое слово вводить

мне кажется (может даже просто приснилось :) что если бы его ввели, то все были бы в восторге, это тоже мое чисто субъективное мнение.

Спасибо за конструктивный ответ!

тем что в это f можно напихать чего угодно, в конце концов

А в делегат нельзя? Вы выше писали:

//вместо std::function f1 = &A::foo;
DelegateType f1 = a.foo;

//где DelegateType надо объявить как особенный тип
Delegate int DelegateType();

Что мне тут мешает написать f1 = b.bar, при условии, что типы совпадают?

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

А в лямбду можно любую дополнительную логику дописать, это же по сути функция чтобы вызвать функцию. Ведь лямбда это функция, только без имени, так ведь? и если кто-то добавит туда логику кроме вызова, ее уже никто не оптимизирует.

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

Они и в моём примере должны, иначе как a.foo внутри лямбы сможет вызваться, если типы переданных в лямбду параметров этого не поволяют? Ответ: никак, т.к. параметры форвардятся с сохранением типа и категории, и если их нельзя использовать для вызова a.foo напрямую, то и кривой вызов через лямбду/std::fucntion сломает сборку.

и если кто-то добавит туда логику кроме вызова, ее уже никто не оптимизирует

С чего бы это? Внимание на ассемблер: https://godbolt.org/z/qdGbfn7En

Они и в моём примере должны, иначе как...

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

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

но я бы не отказался посмотреть на практику применения несуществующего решения

https://habr.com/ru/articles/576052/ — можете сами попробовать :)

можете сами попробовать :)

это то же интересно конечно, хотя мне комментарии про C++ вообще не специфицирует ABI. интереснее, я такого слова "манглингом  " не знал.

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

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

Ну вот же на cppreference:

The lambda expression is a prvalue expression of unique unnamed non-union non-aggregate non-structural class type, known as closure type, ...

The lambda expression is a prvalue expression of unique unnamed non-union non-aggregate non-structural class type, known as closure type,

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

 чтобы были нормальные и не очень медленные делегаты в языке.

во-первых зачем они "в языке", во вторых std::function более чем работает и не "too heavy", его проблемы что нужно копирование + иногда хочется ссылку на функцию, а не значение. Это всё тоже решается и довольно легко (куча библиотек)

Вызвать функцию из указателя куда быстрее, чем из std::function.

Чёто странное, то опшнл у вас в С++11, то пример кода с опнлом, который вообще-то не работает (разыменование забыто), потом какие-то макросы, которые заменяются if (cond) return x;


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

Нда, с std::optional вышла ошибка, спасибо, что указали, я поправил.

Я был так уверен, что четко помню, когда появился std::optional, что даже не удосужился ничего проверить

Не хватает switch constexpr и for constexpr. Ну и конечно, очень жду рефлексию.

По поводу early_return, можно обойтись без макросов вообще. Но тогда придется немножко сломать мозги и перейти к функциональному подходу. И использовать монады. Это несколько более многословно, и не очевидно, но приводит к желаемому результату с сохранением относительной понятности кода. Имплементацию не привожу, так как это очень длинная простыня с огромным количеством шаблонных перегрузок кучи методов, но в целом, если смотреть на пример использования, должно быть интуитивно понятно, что из себя представляет имплементация, и что она, на самом деле тривиальна, даже если не знать, что такое монады.

std::string ApplySpell(Spell *spell) {
  using namespace std::placeholders;
  return Maybe(spell).Or("No spell")
    .Filter(std::bind(&Spell::IsValid, _1)).Or("Invalid spell")
    .FilterOut(std::bind(&Self::IsImuneToSpell, this, _1)).Or("Immune to spell")
    .FilterOut(std::bind(&Self::IsSpellApplied, this, _1)).Or("Spell already applied")
    .Then([](Spell* spell) -> std::string {
      applied_spells_.Append(spell);
      ApplyEffects(spell->GetEffects());
      return"Spell applied";
    });
}

Другой вопрос, стоит ли оно того? Может лучше декомпозировать функцию на отдельно проверки и полезную нагрузку? Да и использовать менее развесистое форматирование для ветвлений?

std::string ApplySpell(Spell *spell) {
  return ValidateSpell(spell)
      .OrElse(std::bind(&Self::ApplySpellImpl, this, std::ref(*spell)));
}

private:
Maybe<std::string> ValidateSpell(Spell* spell) {
  if (!spell) { return "No spell"; }
  if (!spell->IsValid()) { return "Invalid spell"; }
  if (IsImmuneToSpell(*spell)) { return "Immune to spell"; }
  if (IsSpellApplied(*this)) { return "Spell already applied"; }

  return {};
}

std::string ApplySpellImpl(Spell& spell) {
  applied_spells_.Append(spell);
  ApplyEffects(spell.GetEffects());
  return "Spell applied";
}

Лично мне в C++ очень хочется увидеть enum с ассоциироваными значениями, как в rust/swift и паттерн-матчинг, соответственно. Но это очень большие изменения в языке, поэтому я даже не надеюсь на это. Ну и да, compile-time reflection, было бы прекрасно.

Лично мне в C++ очень хочется увидеть enum с ассоциироваными значениями, как в rust/swift и паттерн-матчинг, соответственно.

давайте не будем называть if паттерн матчингом.

visit + variant гораздо сильнее и уже есть в стандартной библиотеке

Мне интересно, как будет выглядеть на variant+visit такой простой пример:

fn do_smth(v: Result<Option<i64>, String>) {
    match v {
        Ok(Some(v)) => {}
        Ok(None) => {}
        Err(v) => {}
    }
}

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

Например как будет выглядеть ошибка, если я удалю ветку с Err(v) => {}:

error[E0004]: non-exhaustive patterns: `Err(_)` not covered
   --> main.rs:12:11
    |
12  |     match v {
    |           ^ pattern `Err(_)` not covered
    |
note: `Result<Option<i64>, std::string::String>` defined here
   --> /home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/result.rs:502:1
    |
502 | pub enum Result<T, E> {
    | ^^^^^^^^^^^^^^^^^^^^^
...
511 |     Err(#[stable(feature = "rust1", since = "1.0.0")] E),
    |     --- not covered
    = note: the matched value is of type `Result<Option<i64>, std::string::String>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
    |
14  ~         Ok(None) => {},
15  ~         Err(_) => todo!()
    |

Result<Option<...

Это уже какой-то антипаттерн.

и тут даже не нужен визит, просто

if (v && *v) use(**v);


Например как будет выглядеть ошибка, если я удалю ветку с 

в visit компилятор заставит обработать все случаи, но в целом это аргумент так себе, т.к. существует default и бах, уже никто ничего вам не напишет

Вообще, то что вы привели это хардкоженные в компилятор вещи, я уж промолчу что вы свой Option на расте написать не сможете, как и result, а на С++ - пожалуйста, пишите под свою задачу получше.

Это уже какой-то антипаттерн.

Возможно, хотя встречается и не сказать что редко.

я уж промолчу что вы свой Option на расте написать не сможете

Точно не смогу? Вроде смог:

enum MyOption<T> {
    Some(T),
    None,
}

enum MyEnum {
    Int(i64),
    NullInt(MyOption<i64>),
}

fn do_smth(v: MyEnum) {
    match v {
        MyEnum::Int(v) => {}
        MyEnum::NullInt(MyOption::Some(v)) => {}
        MyEnum::NullInt(MyOption::None) => {}
    }
}

Вообще, то что вы привели это хардкоженные в компилятор вещи,

Хорошо, удаляем MyEnum::NullInt(OptionI64::None) :

error[E0004]: non-exhaustive patterns: `MyEnum::NullInt(MyOption::None)` not covered
  --> src/main.rs:73:11
   |
73 |     match v {
   |           ^ pattern `MyEnum::NullInt(MyOption::None)` not covered
   |
note: `MyEnum` defined here
  --> src/main.rs:67:6
   |
67 | enum MyEnum {
   |      ^^^^^^
68 |     Int(i64),
69 |     NullInt(MyOption<i64>),
   |     ------- not covered
   = note: the matched value is of type `MyEnum`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
75 ~         MyEnum::NullInt(MyOption::Some(v)) => {},
76 +         MyEnum::NullInt(MyOption::None) => todo!()
   |

Да, опять случай конечно не совсем реальный, но все же с вами не соглашусь. На реальный можно глянуть например сюда и сюда. На более сложных примерах я вообще удивляюсь, как подробно и понятно компилятор объясняет почему я не прав. Да, в редких случаях может выдать что-то не очевидное (как-то с GAT имел дело, та еще мистика, особенно когда там еще с lifetime-ми надо поработать).

и тут даже не нужен визит,

Да, в этом примере может и не нужен, но это уже из-за того что пример построен на стандартных enum-ах.

в visit компилятор заставит обработать все случаи

Интересно увидеть, как будет выглядеть ошибка. Сколько сотен строк с объяснениями как компилятор пытался подставить типы в шаблоны, да ничего не вышло. Вполне возможно что в современном C++20 это выглядит читаемо, поэтому интересно увидеть какой-нибудь такой пример.

Точно не смогу? Вроде смог

так вы и использовали фактически встроенный в язык Option, ниже уровнем вы не можете опустится даже с всякими unsafe::union (существование которого это какой-то абсурд), в рамках языка как раз не выйдет

Ну и visit вы не сможете написать даже для стандартного Option

visit([](auto&& x) { x.foo(); }, var);

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

На расте невыразимо

Верно. Хотя можно сделать что-то альтернативное, но это все же не то, что вам надо:

trait Foo {
    fn foo(&self);
}

fn do_smth(v: impl Foo) {
    v.foo();
}

Для enum конечно еще придется попотеть, чтобы интерфейс Foo руками реализовывать не пришлось:

// А еще надо реализовать сам derive(Foo).
#[derive(Foo)]
enum MyEnum {
    A(TypeA), // TypeA реализует Foo
    B(TypeB), // TypeB реализует Foo
}

В любом случае, да, в Rust подобное невыразимо. Плохо ли это? Лично мне такая возможность в C++ и не нравится, так как за нее нужно платить. Например, чтением сотен строк непонятных ошибок, с чем я периодически сталкиваюсь и могу застрять на часок другой.

Вообще, на тему достоинств и недостатков различных моделей дженериков (а у Rust и C++ они различаются) есть хорошая статья.

так вы и использовали фактически встроенный в язык Option, ниже уровнем вы не можете опустится

Называть встроенный в язык tagged union чем-то плохим, что нельзя реализовать самому, очень странно. На стандартном C++ вон не получится реализовать свои функции, и какой вывод из этого надо сделать?

Ну и visit вы не сможете написать даже для стандартного Option

Если у вас одинаковое поведение для разных членов Option, то это можно вынести в отдельный тип уровнем выше. А вообще все эти вещи отлично абстрагируются через линзы/призмы, которые на хаскеле (и, возможно, на расте) делаются легко через адекватное метапрограммирование, а не как в плюсах.

Это похоже на ту самую оптику?

Внизу статьи ссылки на имплементации, может и есть где-то.

Вообще, то что вы привели это хардкоженные в компилятор вещи, я уж промолчу что вы свой Option на расте написать не сможете, как и result

Шта?

pub enum Option<T> {
    None,
    Some(T),
}
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Эти реализации описаны в первых же строках документации по Option и Result, и каждый может сделать свой MyOption и MyResult, если ему так вздумается. Вопрос лишь в том, зачем. И из него вытекает второй вопрос: что ваш неверный аргумент доказал бы?

Я всеми руками за функциональное программирование, но надо признать что функциональный код на С++ выглядит ужасно. Уж лучше исключениями бросаться.

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

Да, я понимаю, что концепты более общие, чем просто описание интерфейса функций. Но мне все равно, это не так читаемо и, к тому же, это не сочетается с описанием интерфейсов для динамического полиморфизма. Это уже странно. Зачем нам два способа описать интерфейс, чтобы в компайл тайме показать компилятору что какой-то тип ему соответсвует? What?

А слабые дженерики это просто способ не стрелять себе в ногу, когда вдруг решил поменять шаблонный параметр при вызове своей функции. К тому же это еще и способ описать интерфейс типа, который функция будет использовать. А то читать код библиотек, где написано <typename T> не очень приятно.

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

Ну ладно, минутка нытья по поводу C++ закончилась)

это не сочетается с описанием интерфейсов для динамического полиморфизма

в расте тоже не сочетается, не столько потому что дженерики плохие, просто мир так устроен, что-то сделать невозможно

А слабые дженерики это просто способ не стрелять себе в ногу

Ещё это способ сделать все библиотеки хуже, все стандартные интерфейсы сложнее и опять же хуже (см. итераторы, контейнеры, сравните в С++ и в расте и при должном понимании осознаете)

Если вы хотите сочетать концепты и динамическое, то вот:

https://github.com/kelbon/AnyAny

Не генерируется автоматически из концепта конечно(потому что невозможно), но дак тайпинг (с большими чем у раста возможностями) предоставляет

в расте почти сочетается и это круто. Хотя я могу ошибаться с этим.

Но с первого взгляда - пишешь <T: Sortable> и dyn Sortable. Довольно хорошо сочетается.

Не понял почему хуже будет библиотеки. Примерно реализацию что там что там я знаю. Объясните, если не затруднит?

Вашу библиотеку видел, спасибо. Мне понравилась.

Но с первого взгляда - пишешь 

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

Насчёт библиотек назову лишь основные вещи, которые именно из-за дженериков не могут быть реализованы хорошо -

  • аллокаторы у каких-то контейнеров есть, у каких-то нет, зависит исключительно от того получилось ли написать это на растовых дженериках

  • компараторы также

  • нельзя расширять/специализировать позже без изменения АПИ, например в С++ после 17 стандарта алгоритмы можно стало оптимизировать для последовательной памяти, при том что интерфейс принимает input iterator, в расте это невозможно, взял инпут итератор - используй только то что у него есть, это ухудшает перфоманс (и поддерживаемость кода)

  • раст просто набит макросами и кодгеном на них, например макрос который реализует функцию для всех туплов до 16 типов в них (вариадиков то нет)

  • в расте по сути есть только 2 вида итераторов - инпут итератор и contiguous (slice), почему? Потому что не выражается иерархия итераторов из С++ на расте. Поэтому, например, сортировка может быть сделана только на последовательной памяти, сортировать random_access_range уже не выйдет (это например deque, unordered_map<index, value>, flat map и тд)
    Также раст итераторы максимально примитивные, у них внутри только value_type, из-за этого невозможно сделать контейнер с прокси ссылкой (как flat_map или vector<bool>)

    И изменить это позже нельзя, т.к. например iterator_traits на расте написать невозможно. Там впринципе условие "или" в дженериках нереализуемо

хм, спасибо за ответ.

Но без подробностей я мало понял. Допустим вы правы. Но есть несколько вопросов.

Зачем сортировать unordered_map? По моему это не имеет смысла с точки зрения самой идеи хэштаблицы.

А нужен ли вам способ сделать "или" в определении дженерика? Как функция должна его использовать? Написать if constexpr и вызвать что-то в зависимости от типа? Тогда я думаю вам нужно две функции.

В раст ведь есть ассоциированные типы. С ними вполне можно реализовать iterator_traits

Вообщем можно долго дискутировать что лучше: интрузивные или не... интерфейсы.

Зачем сортировать unordered_map?

Это условно, но можно применить sort_copy например, копируя в другое место

Можно придумать iota | transform, это крайне популярный паттерн для бинарного поиска, который также random access range и также только на последовательной памяти в rust

А нужен ли вам способ сделать "или" в определении дженерика?

В раст ведь есть ассоциированные типы. С ними вполне можно реализовать iterator_traits

Как раз чтобы реализовать iterator_traits нужно иметь "ИЛИ", более того, нужно вообще другую модель дженериков, т.к. в расте все трейты нужно явно реализовать для каждого типа, а итератор ничего не знает про iterator_traits

Например

// псевдокод
template<typename It>
using difference_type = iterator::difference_type
                        || decltype(It - It)
                        || incrementable_traits<It>::difference_type;

Ещё это способ сделать все библиотеки хуже, все стандартные интерфейсы
сложнее и опять же хуже (см. итераторы, контейнеры, сравните в С++ и в
расте и при должном понимании осознаете)

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

Помню выжимку по работе комитета для с++20: начали работу над пропозалами для переноса Boost.Process в стандарт, переделку работы с файлами и вводом/выводом, linealg, юникод, 2d графика, аудио, JIT компиляцию C++ кода. Скажите, а вот это все действительно нужно в стандарте?
Все было прекрасно до c++17, сейчас просто хочется сказать "горшочек не вари"

Мне все больше кажется что начиная с  c++17 это уже не С++, это пародия на Java с указателями и с хидер-файлами.

Если без восторга прочитать статью, она о чем:

  • как нам не хватало функции для работы со строками,

  • конструкции для работы с enum -ами

  • конструкции для работы с битовыми представлениями

  • early_return тот еще шедевр, как мы без него жили раньше, срочно переписывайте мегабайты кода

  • std::expected std::optional - вау! еще пару темплейтов добавили в библиотеку, визжим от восторга, как мы без них жили!

Это вот прям именно то для чего создавался С++, извините но по моему это какое-то развитие в зад...

ницу, опечатка, что-то про пятницу не получилось написать.

начали работу над пропозалами

В такой формулирвоке это значит только то, что их рассматривают, а не то, что их готовят для ввода в стандарт.

Lifehack: не ждать когда всё добавят в стандарт, и не изобретать свои костыли, а взять качественные промышленные костыли сделанные кем-то большим. Скажем absl от Гугла, или folly от Фейсбук. Там многое из перечисленного было давно добавлено, и много такого небольшого, но полезного, что нужно очень часто. Плюс они в standards committee и их костыли часто оказываются в какой-то версии стандарта уже в std.

Чтоб быть точным, язык Rust создавался под большим влиянием... нет, не Haskell, а F#, .NET адаптации языка OCaml, старшего ML-брата Haskell.

type 'a option = None | Some of 'a
type ('a, 'b) result = Ok of 'a | Err of 'b

let a = Some 2;

Насколько мне известно, Rust создавался под впечатлением (в частности) от OCaml непосредственно, и на OCaml даже была написана первая версия компилятора. Зачем вы его называете "старшим ML-братом Haskell" - неясно, в обоих языках есть фичи, которых нет в другом.

Поддержку модулей слишком долго не вводили.

И потом на курсах предлагают С++ за полгода изучить? Ну-ну.

Действительно смешно. Больше семестра на С++? Наверно они не знают как организовать 6 пар в день, а туда же, курсы изобретать…

Они предлагают, в основном, заплатить за курс деньги.

Пример с биткастом будет работать, только если sizeof(float) = sizeof(long), что не гарантировано стандартом и не взлетит на Linux :) Вообще стандарт очень мало чего гарантирует, даже то, что float 32-битный, и то не факт.

Зато если он не будет работать, вы узнаете об этом в компилтайме даже не в констекспр-контекстах, а это хорошо.

  • uint32_t i = *reinterpret_cast<uint32_t*>(&f)

  • uint32_t i = *(uint32_t*)&f;

Если f это float f; и если это не какая-нибудь экзотическая архитектура, то конкретно в этих строках UB нет.

  • Трюк с union:

    union {
    	float f;
    	uint32_t i;
    } value32;
    

    Сам по себе такой код не UB, но беда в том, что чтение из union-поля, в которое вы перед этим ничего не писали — это тоже UB.

Само по себе чтение из неинициализированной переменной не является UB.

Во всех случаях проблем с памятью нет. Всё зависит от того как вы собираетесь использовать этот код. Впрочем, как и любой другой в C/C++.

Если f это float f; и если это не какая-нибудь экзотическая архитектура, то конкретно в этих строках UB нет.

Есть. В C++ type punning не через char и пару других типов — это UB.

Само по себе чтение из неинициализированной переменной не является UB.

А чтение из неактивного члена union'а — является.

По вашей ссылке куча примеров отстрела ноги (при чём не всегда отстрела), но она не является релевантной к прокомментированным мной кускам кода приведённых автором статьи, т.к. описивыет использование значений. В том, что прокомментировал я значения не используются. А существуют варианты использования этих значений эффективно и безопасно.

Если вы хотите обсудить конкретный пример (там их много) по вашей ссылке, укажите на него.

По вашей ссылке куча примеров отстрела ноги (при чём не всегда отстрела), но она не является релевантной к прокомментированным мной кускам кода приведённых автором статьи, т.к. описивыет использование значений. В том, что прокомментировал я значения не используются. А существуют варианты использования этих значений эффективно и безопасно.

Пожалуйста, ознакомьтесь с правилами использования reinterpret_cast:

https://en.cppreference.com/w/cpp/language/reinterpret_cast

Особенно с вот этим абзацем:

5) Any object pointer type T1* can be converted to another object pointer type cv T2*. This is exactly equivalent to static_cast<cv T2*>(static_cast<cv void*>(expression)) (which implies that if T2's alignment requirement is not stricter than T1's, the value of the pointer does not change and conversion of the resulting pointer back to its original type yields the original value). In any case, the resulting pointer may only be dereferenced safely if allowed by the type aliasing rules (see below)

Так вот, в данном случае указатель не может быть dereferenced safely, потому что нельзя просто так алиасить floatкuint32_t. Вне зависимости от того, как вы собираетесь использовать полученное значение.

which implies that if T2's alignment requirement is not stricter than T1's,
the value of the pointer does not change and conversion of the
resulting pointer back to its original type yields the original value

Размер и выравнивание int и float одинаковые.

In any case, the resulting pointer may only be dereferenced safely if allowed by the type aliasing rules (see below)

Смотрим ниже:

union U { int a; double b; } u = {0};

...

int* p3 = reinterpret_cast<int*>(&u); // value of p3 is "pointer to u.a": // u.a and u are pointer-interconvertible   double* p4 = reinterpret_cast<double*>(p3); // value of p4 is "pointer to u.b": u.a and // u.b are pointer-interconvertible because // both are pointer-interconvertible with u  

Ещё более жёсткий пример, чем int и float должен работать нормально.

Ещё более жёсткий пример, чем int и float должен работать нормально.

Так тут же не происходит разыменования указателей. А по поводу разыменования есть пример ещё ниже:

double d = 0.1;
std::int64_t n;
static_assert(sizeof n == sizeof d);
// n = *reinterpret_cast<std::int64_t*>(&d); // Undefined behavior

Размер и выравнивание одинаковое, а разыменовывать получившийся указатель все равно нельзя. И по поводу union у вас тоже бред написан, ибо it is undefined behavior to read from the member of the union that wasn't most recently written. В комментарии ниже вы ни разу не записали ничего ни в один из членов union value32, и соответственно оба чтения их обоих членов union у вас приводят к UB. Учите матчасть.

P.S. И кстати вот это - тоже бред:

Само по себе чтение из неинициализированной переменной не является UB.

Это как раз хрестоматийный пример UB, см. случай Uninitialized scalar здесь.

Это как раз хрестоматийный пример UB, см. случай Uninitialized scalar здесь.

Uninitialized scalar

std::size_t f(int x)
{
    std::size_t a;
    if (x) // either x nonzero or UB
        a = 42;
    return a;
}

В четвёртой строке нет никакого UB. и в этой функции нет UB. UB возможно при использовании значения, которое вернёт эта функция, потому, что она может вернуть мусор, если её об этом попросят передав ей 0. Может быть разработчик хочет получить случайное число в этом случае, а вы утверждаете, что это UB. А может он хочет узнать, что было на стеке в данном месте за мгновение до вызова и он это с некоторой вероятностью узнает.

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

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

Дайте ссылку на стандарт, где написано, что такой алгоритм недопустим.

Этот пример про другое

int f(bool b) {
unsigned char c;
unsigned char d = c;
int e = d;
return b ? d : 0;
}

В нём даже разные длины. И внём про кастование, а в Uninitialized scalar про мусор.

Пример про другое, но можно и сам пункт прочитать:

If an indeterminate value is produced by an evaluation, the behavior is undefined except in the following
cases

return a; — тут и происходит то самое evaluation.

Вы уже не в первый раз не способны прочесть текст по ссылке, отсюда и весь тот бред, который вы несёте.

std::size_t f(int x)

Было бы говорящее название у функции и всё было бы нормально.

if (x) // either x nonzero or UB

Чтобы не быть голословным в таких утвержениях нужно приводить включающий код и навешивать ярлык UB там, где он проявляется, а не где попало. А так любое if (x) становится UB.

UB это в том числе и разрешение компилятору делать что угодно c виновным в UB кодом. В корректном коде UB быть не должно, а с некорректным можно не церемониться.
В данном случае, например, компилятор может и не компилировать условие. Вполне возможно, что с точки зрения компилятора условие всегда выполняется, ведь иначе будет UB, чего быть не может.
И функция выродится в return 42

Так тут же не происходит разыменования указателей.

Представьте, что вы кладовщик. И у вас на складе есть коробки, которые могут содержать int и нет коробок, которые могут содержать float, хотя размеры вроде бы одинаковые. На склад пришла фура с float. Скажете начальнику: "Нет у меня коробок для float!"? Или поразмыслите как не стать безработным?

void print(float f, int i)
{
    printf("%f\n%d\n\n", f, i);
}

int to_storage(float f)
{
    return *reinterpret_cast<int*>(&f);
}

float from_storage(int i)
{
    return *reinterpret_cast<float*>(&i);
}

int main(int argc, char* argv[])
{
    float f{1.f};
    int i = to_storage(f);
    print(f, i);
    float f_other = from_storage(i);
    print(f_other, i);

    if(f == f_other)
        printf("success\n");

    return 0;
}

Вывод:

1.000000
1065353216

1.000000
1065353216

success

Какая вам разница от того как выглядит значение float в коробке типа int?

Где здесь UB?

С объединениями получится элегантнее.

Вы поймите, что эти ваши аналогии про кладовщиков и прочие фантазии не имеют никакого отношения к суровой реальности, в которой компилятор имеет право выполнять любые оптимизации в расчёте на то, что вы не делаете ничего подобного тому, что вы привыкли, по-видимому, делать. То что это сейчас у вас по случайному совпадению работает без специальных ключей компилятора типа -fno-strict-aliasing - не более чем счастливое для вас совпадение. Сейчас работает, а в следующей версии компилятора или в немного другом случае уже не работает. Ваши фантазии глупые и вредные, но это вам ещё даст по башке, и не раз. Нет смысла тратить на вас время.

А чтение из неактивного члена union'а — является.

Укажите на строку в которой UB:

union {
	float f;
	uint32_t i;
} value32;

float f = value32.f;
uint32_t i = value32.i;

Обе две последних. [basic.life] в стандарте говорит, что лайфтайм объектов начинается в случае юнионов только тогда, когда вы их инициализируете списком инициализации [dcl.init.aggr], в них пишете или делаете placement new [class.union], либо в ещё паре нерелевантных случаев. На двух последних строчках справа от знака равенства происходит lvalue-to-rvalue conversion, а [conv.lval] вместе с [basic.life]/7.1 требует, чтобы лайфтайм конвертируемого объекта уже действовал, чего здесь не происходит.

Да? А я думал, что место в памяти выделяется в строке:

} value32;

и дальше мы можем делать сней что угодно.

Это классика. C++ оперирует объектами, а не байтами памяти.

В C — возможно, я не знаю C. Но в C++ место в памяти и время жизни объекта — это две разные сущности, которые не обязаны совпадать (и совпадают далеко не всегда).

Мы всё ещё про фундаментальные типы говорим?

В том числе.

char a[sizeof(int)]{ }; // в массиве лежат объекты типа char
auto iptr = new (a) int(5); // теперь там лежит int
auto cptr = new (a) char('c'); // время жизни *iptr закончилось

Ни в коем случае не придирка, просто для того, чтобы показать, что все еще серьезнее: для a не хватает alignas, имхо.

А вообще респект вам за то, что у вас хватает терпения тов@cortll прописные истины объяснять.

Да, про выравнивание забыл, спасибо.

Ответ тот же:

По вашей ссылке куча примеров отстрела ноги (при чём не всегда отстрела), но она не является релевантной к прокомментированным мной кускам кода приведённых автором статьи, т.к. описивыет использование значений. В том, что прокомментировал я значения не используются. А существуют варианты использования этих значений эффективно и безопасно.

Если вы хотите обсудить конкретный пример (там их много) по вашей ссылке, укажите на него.

С enum count шикарно - но это же жесть - столько времени тратить на все эти обвязки.

В дополнение к starts_with, ends_with еще довольно часто не хватает trim

Забыли преобразование enum в строку и наоборот. Каждый до сих пор пилит свои велосипеды для этого.

Ещё бесит, когда надо получить массив без дубликатов. Сейчас это делается вот так:

std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());

Это весьма многословно и хуже того подвержено ошибкам. Хотелось бы для этого иметь какой-либо стандартный метод.

Ещё хуже, что это O(nlogn), хотя для дедупликации достаточно O(n) (пусть и ценой O(k), k — число уникальных элементов, памяти, но на что обычно плевать).

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

Так и не выбрался из джунов. Вот таким должно быть сравнение начала

inline bool starts_with(const std::string &s1, const std::string &s2)
{
	return !s1.rfind(s2, 0);
}

Это шутка какая-то? Сделал полностью нерабочее решение, которое ещё и делает поиск внутри всей строки, вместо только начала

https://godbolt.org/z/nsns843Ws

P.S. зачем вы сделали "заумно", вместо haystack.find(needle) == 0 ?

(не делайте так, это поиск по всей строке вместо сравнения только в начале)

Да, чувак, да ты полный ноль. С чего это "hello" будет начинаться с "hello world". Ты перепутал аргументы. RTFM, лошара. Где там поиск внутри всей строки назад от первого символа.

Переменные называть научись, s1 s2,

Учу читать:
starts_with(A, B)
B Начинается с A

Да это извращение какое-то, никто так не читает. Объявление и семантика функции взяты из статьи. Ты ещё не читал стандарт, там просто могут быть c и d.

Ну так выбирайтесь :)

Вкусовщина, но мне однажды в университете понадобились BigInteger

Дело было в 20-ом году, в других языках они давно были, а C++ до сих пор нет

в бусте есть

А вы пробовали использовать буст в реальном проекте? Очень в редких случаях получается взять пару хедеров в проект, и не притащить туда весь буст. Ну это ладно, но со временем хедеры буста начинают оплетать проект как паутина, а время компиляции летит в космос. А хотели затащить только хедер для работы со временем

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

Хочу переменную со значением 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF. Подскажите, как её в ваш BigInteger поместить?

Понятия не имею.
- "ваш" - это обращение к группе. Ну то есть не к конкретному человеку. Ну то есть ответственного нет.
- Автор термина 'BigInteger' в этом топике - MtTheDamos, что он имел ввиду под этим термином - к нему вопросы. Может int64, может uint64, может xint128, может как в питоне без ограничений, я х.е.з.

UB

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

Мне нужны были числа Эйлера.
Я невнимательно прочитал задание, поэтому нашёл аж 243 числа.
А надо было только первые 30.
Если интересно, то 30-ое выглядит так:
12622019251806218719903409237287489255482341061191825594069964920041
А 243-е вот так:
1552754587364941454091550628784573284531417585974177462661665696581096507450323154161877012935634703115951325817420960313543117775844223174861285058216045137219151106252982387332536816251605356614666841757142282287222638475081488010899740219873267392580833806849633909800740090249332088318254353071347881225103802667657014130960756876086387423475394715873819555706637695073767542791670601109181667857352033359043188645495812422523272030548619985258815858118870195783453230356977669973606497306955094418814719082309467098992785483255018790754552225354691932914305234629248862187055107065031861150000572886966684462489433279684361321459069012573266036514127409836511791025455800207112508265787428351943299214579044828616228994840116871643043913618195850146815307497383497541328636894508200652455076599811968923451876201721405398725502150549795883247394980411420203229056814391742354498516096156540857112971088520588190469400457085900836559263116579625640443055308956373497319145122411468464451285605
Когда я вставил первые 243 в Visual Studio, то он крашнулся, а когда я пролистывал мимо них, то начинало лагать. Жаль не пригодились они, эпично было бы

C - скальпель.
C++ - скальпель с обрезиненной ручкой.
Не стоит размахивать скальпелем везде подряд куда попало.
Прежде всего Вы порежетесь.

Вам нужно нечто вроде пассатижей, поэтому Java/C#/Python/PHP (пардон)/JS и прочая фигня.
Или читать документацию.

PS. а еще лучше начать с Ассемблера

На этих лабораторных (предмет назывался "Высокопроизводительные вычисления") нельзя было выбрать язык программирования - только C++
А когда в следующий раз я работал с большими числами (это были лабораторные по шифрованию) я использовал Python

Хороший тамада. И конкурсы интересные.

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

  • Python - быстрое макетирование. Для MVP (Minimum Viable Product)

  • C++ - Скорость почти как C и удобства почти как Python. Но именно почти. Для первого релиза.

  • C - почти Assembler. Куча мороки, но скорость почти на пределе. Но там всё ручной работы.

  • Assembler - ну тут понятно.

В принципе выбор C++ для "Высокопроизводительные вычисления" вполне себе. Баланс между "яжнепрограммист" и скоростью.

Но не ждите от C++ заноса хвоста на всех поворотах. Это уже не C, но от питона оно еще дальше.

В принципе выбор C++ для "Высокопроизводительные вычисления" вполне себе. Баланс между "яжнепрограммист" и скоростью.

Но не ждите от C++ заноса хвоста на всех поворотах. Это уже не C, но от питона оно еще дальше.

Интересно, а какие преимущества у чистого Си перед C++ в "высокопроизводительных вычислениях" кроме наличия ключевого слова restrict?

И это был отличный вопрос.
Ответ: ни у каких ЯП ни перед какими ЯП нет никаких преимуществ и/или недостатков. У отвертки нет абстрактного тотально преимущества над молотком. У молотка надо отверткой тоже. Пассатижи курят в стороне задумчиво и думают "не дай бог...".

Ответ: ни у каких ЯП ни перед какими ЯП нет никаких преимуществ и/или недостатков.

Ню-ню. Ню-ню.

Попробуйте поделать высокопроизводительные вычисления на языках, в которых нельзя отключить ран-тайм проверки (типа валидности индексов или переполнения чисел). И в которых программист вообще не имеет возможности воздействовать на расположение данных в памяти.

Ну и вас не смущает то факт, что ваша фраза "ни у каких ЯП ни перед какими ЯП нет никаких преимуществ и/или недостатков" тупо делит на ноль ваш же перечень языков с их характеристиками (типа Python -- это для MVP, а Assember -- "ну тут понятно")? Ведь если нет преимуществ, то подобное ранжирование бессмысленно.

Ох уж эти свидетели скорости Си и его близости к ассемблеру.

Можете подтвердить свои слова кодом на Си и кодом на Си++, который делает одно и то же, но Си выигрывает?

early_return

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

Почему интерфейс вектора не располагает такой простой альтернативой обычному методу erase, не понятно. В Rust вот, например, он есть.

потому что erase-remove idiom мешает. Ну и плюс там итераторы везде, которые знаю только про себя, но не про конец. Опять же алгоритмы устроены так, что в тот же std::remove можно передать только кусок вектора, как из него pop_backать тогда, если конец где-нибудь на середине оказался. Вот так и живём.

Вообще очень недостаёт Range-based алгоритмов и их адекватного пайпинга.

Ну а касательно опционалов - если вы пишите кроссплатформенные приложения, то семанитика использования опционалов может различаться в зависимости от компилятора. Помнится там то ли .has_value() то ли .value_or() был на яблочном шланге, но отсутствовал в msvc компиляторе. Из-за этого пришлось уродовать большим количеством кода.

Ну а пример со Spell как обычно просит распилить это и гонять все это через ECS.

Куча всего была в бусте сильно задолго до 13-го года.

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

У популярных компиляторов версия языка привязана к версии стдлиб. Стандарт на язык точно содержит раздел про стыдлиб, с C++03 точно, для более ранних не уверен.

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

Автор не указал C++-что он имеет ввиду - язык, стандартную библиотеку или компилятор.

Например в том же питоне с каждой версией языка появляются новые конструкции.
Меняется именно язык.
При этом стандартная библиотека меняется отдельно. Хотя да, идет в комплекте.
Про трансляторы молчу, их не один.

Автор специально сделал в статье disclaimer, чтобы индульгировать себя от фанатов терминологии и комментариев на эту тему. Но автор недооценивал комментаторов

Автор довольно тщательно спрятал Disclaimer, хороший.
Читатели не внимательные, не хорошие.

Стандартная библиотека C++ — это часть языка C++. Стандарт C++ описывает стандартную библиотеку в том числе

Стандартная библиотека C++ - это часть стандарта C++.
Не языка. На синтаксис и поведение библиотеки не влияют.

Язык C++ — это то, что описывается стандартом C++, поэтому не вижу разницы.

Ну и на поведение, конечно, эти библиотеки влияют, потому что некоторые языковые вещи требуют поддержки библиотек (`std::initializer_list`, всякая корутиновая ерунда, некоторые реализованные компиляторными интринсиками type traits, и так далее).

это то, что описывается стандартом C++, поэтому не вижу разницы

Показываю разницу: общее != частное.

Стандарт описывает язык и stdlib.
Стандарт языка не включает stdlib.

Для примера можете снести у себя libstd++-devel.
И следите за руками:

echo "int main(void) {int a = 2*2; return 0;}" > test.cpp && gcc test.cpp

Оно собирается и работает.
Без стандартных библиотек.

Стандарт описывает язык и stdlib.
Стандарт языка не включает stdlib.

Звучит так, будто у вас есть отдельно стандарт и отдельно стандарт языка. Это не так, есть единственный стандарт для каждой версии (вроде ISO/IEC 14882:2020), и там описывается и язык, и стандартная библиотека. Нет отдельного «стандарта языка».

Оно собирается и работает.

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

void foo() { typeid(int); }

Эта площадка слишком токсичная для дискуссий.
Я согласен с Вами полностью и абсолютно со всем.

Перечисление не может иметь кастомных значений типа:

Если использовать сишный enum а не enum class - то можно.

Мы вынуждены использовать enum class, так как иначе нельзя будет создать элемент COUNT в более чем одном енуме.

Проблема кастомных значений типа заключается в том, что размер енума будет вычисляться неправильно. Если написать что-то вроде

enum class MyEnum { A = 10, B, C, COUNT };

то это будет эквивалентно

enum class MyEnum { A = 10, B = 11, C = 12, COUNT = 13 };

Это приводит к тому, что мы не можем использовать COUNT для вычисления размера енума (в результате работы программы будет выведена строка "3 13"):

template<typename EnumT>
constexpr int enum_size() {
  return static_cast<int>(EnumT::COUNT);
}

enum class GoodEnum { A, B, C, COUNT };
enum class BadEnum { A = 10, B, C, COUNT };

int main() {
  printf(
    "%d %d\n",
    enum_size<GoodEnum>(),
    enum_size<BadEnum>()
  );
}

Давайте посмотрим на типичную функцию с некоторым количеством проверок на пограничные состояния:

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

Оригинальный код
#include <string>
#include <vector>
#include <algorithm>

struct Effects {};
struct Spell {
    bool isValid() const {
        return true;
    }
    Effects getEffects() const {
        return {};
    }
};

struct Example {
private:
    std::vector<Spell*> appliedSpells;
    void applyEffects(Effects) {}
public:
bool isImmuneToSpell(Spell*) {
    return false;
}
std::string applySpell(Spell* spell)
{
	if (!spell)
	{
		return "No spell";
	}

	if (!spell->isValid())
	{
		return "Invalid spell";
	}

	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

	// if (this->appliedSpells.constains(spell))
    if (std::find(appliedSpells.begin(), appliedSpells.end(), spell) != appliedSpells.end())
	{
		return "Spell already applied";
	}

	appliedSpells.push_back(spell);
	applyEffects(spell->getEffects());
	return "Spell applied";
}
};

Для начала, зачем вообще принимать Spell по указателю? Указатель может быть null, и это то, что нам никогда не нужно и в данном контексте всегда является ошибкой. А посему можно принимать Spell по значению, и в этом случае бремя доказательства наличия заклинания лежит на вызывающей стороне. (В реальном коде принимали скорее по &&-ссылке, но ссылка также не может быть null). Имеем:

Код без указателей
#include <string>
#include <vector>
#include <algorithm>

struct Effects {};
struct Spell {
    bool isValid() const {
        return true;
    }
    Effects getEffects() const {
        return {};
    }
    auto operator<=>(Spell const&) const = default;
};

struct Example {
private:
    std::vector<Spell> appliedSpells;
    void applyEffects(Effects) {}
public:
bool isImmuneToSpell(Spell const&) {
    return false;
}
std::string applySpell(Spell spell)
{
	if (!spell.isValid())
	{
		return "Invalid spell";
	}

	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

    if (std::find(appliedSpells.begin(), appliedSpells.end(), spell) != appliedSpells.end())
	{
		return "Spell already applied";
	}

	appliedSpells.push_back(spell);
	applyEffects(spell.getEffects());
	return "Spell applied";
}
};

Раз - и первый if ушёл. Бонусом получили вызов методов через точку вместо стрелочки, а ещё из-за требования предоставить оператор сравнения проявили тот факт, что сравниваются указатели на заклинания вместо самих заклинаний. Валидным такое поведение будет являться только в том случае, если мы все заклинания интернируем.

Следующий if - это вызов isValid. Сам факт наличия такого метода является ошибкой дизайна. Именно, как так получилось, что у нас есть тип Spell, который может содержать что-то, что не является заклинанием? Возможность создать невалидное заклинание означает, что валидность нужно проверять снова и снова, и вызывающий код должен эти ошибки обрабатывать, вне зависимости от того, возвращаются ли они через коды возврата или исключения. Проверку валидности нужно переместить туда, где ей самое место: в конструктор Spell:

struct Spell {
private:
    struct private_tag {};
    std::string name;
    Spell(private_tag, std::string_view name): name(name) {}
public:
    static Spell construct(std::string_view name) {
        if (name == "invalid") {
            throw std::invalid_argument("not a valid spell");
        }
        return Spell(private_tag {}, name);
    }
    // прочие методы
}

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

Новый код без второго if:

Код с валидацией в конструкторе
#include <string>
#include <string_view>
#include <vector>
#include <algorithm>
#include <stdexcept>

struct Effects {};
struct Spell {
private:
    struct private_tag {};
    std::string name;
    Spell(private_tag, std::string_view name): name(name) {}
public:
    static Spell construct(std::string_view name) {
        if (name == "invalid") {
            throw std::invalid_argument("not a valid spell");
        }
        return Spell(private_tag {}, name);
    }
    Effects getEffects() const {
        return {};
    }
    auto operator<=>(Spell const&) const = default;
};

struct Example {
private:
    std::vector<Spell> appliedSpells;
    void applyEffects(Effects) {}
public:
bool isImmuneToSpell(Spell const&) {
    return false;
}
std::string applySpell(Spell spell)
{
	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

    if (std::find(appliedSpells.begin(), appliedSpells.end(), spell) != appliedSpells.end())
	{
		return "Spell already applied";
	}

	appliedSpells.push_back(spell);
	applyEffects(spell.getEffects());
	return "Spell applied";
}
};

Следующий if - это проверка на наличие иммунитета к заклинанию. Как пишет автор:

Несчастные три строчки внизу — реальная работа метода. Остальное — проверки, можно ли совершить эту работу.

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

Следующий if проверяет, есть ли заклинание в наборе уже применённых, и делает по этому условию возврат, если оно уже есть. В противном случае заклинание добавляется. Иными словами, набор заклинаний уникален. А знаете, какая есть структура данных, которая поддерживает этот инвариант? Множество! Более того, эта структура данных имеет меньшую асимптотику для поиска значения, чем вектор, что может стать более эффективным, когда число заклинаний вырастет до пары тысяч или около того.

Что ж, заменим std::vector на std::unordered_set:

auto [appliedSpell, inserted] = appliedSpells.insert(spell);
if (!inserted)
{
    return "Spell already applied";
}
applyEffects(appliedSpell->getEffects());
return "Spell applied";

Разумеется, это потребует написать хешер. Но мы можем просто делегировать это хешу от имени заклинания:

template<> struct std::hash<Spell> {
    auto operator()(Spell const& s) const {
        return std::hash<std::string>{}(s.getName());
    }
};

Новый код:

Финальная версия
#include <string>
#include <string_view>
#include <unordered_set>
#include <algorithm>
#include <functional>
#include <stdexcept>

struct Effects {};
struct Spell {
private:
    struct private_tag {};
    std::string name;
    Spell(private_tag, std::string_view name): name(name) {}
public:
    static Spell construct(std::string_view name) {
        if (name == "invalid") {
            throw std::invalid_argument("not a valid spell");
        }
        return Spell(private_tag {}, name);
    }
    std::string const& getName() const {
        return name;
    }
    Effects getEffects() const {
        return {};
    }
    auto operator<=>(Spell const&) const = default;
};

template<> struct std::hash<Spell> {
    auto operator()(Spell const& s) const {
        return std::hash<std::string>{}(s.getName());
    }
};

struct Example {
private:
    std::unordered_set<Spell> appliedSpells;
    void applyEffects(Effects) {}
public:
bool isImmuneToSpell(Spell const&) {
    return false;
}
std::string applySpell(Spell spell)
{
	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

    auto [appliedSpell, inserted] = appliedSpells.insert(spell);
    if (!inserted)
    {
        return "Spell already applied";
    }

	applyEffects(appliedSpell->getEffects());
	return "Spell applied";
}
};

Покажу отдельно итоговый applySpell:

std::string applySpell(Spell spell)
{
	if (this->isImmuneToSpell(spell))
	{
		return "Immune to spell";
	}

    auto [appliedSpell, inserted] = appliedSpells.insert(spell);
    if (!inserted)
    {
        return "Spell already applied";
    }

	applyEffects(appliedSpell->getEffects());
	return "Spell applied";
}

Осталось только два if-а, оба нужны для логики метода. Может ли тут пригодиться краткая запись для early return? Да, но, на мой личный взгляд, тут проблема стоит уже не так остро, особенно с учётом того, как похудел метод.

В общем, простите, автор, но в необходимости наличия краткой записи early return вы меня не особо убедили.

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

В общем, оба мы здорово провели время :)

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

Более приближенный к реальности пример был бы убедительнее.

static Spell construct(std::string_view name)

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

Моё C++-кунг-фу недостаточно сильно. Я не разобрался, как исполнить какой-то код до вызова делегирующего конструктора.

А зачем? Можно же сделать так:

Spell(std::string arg)
{
  if ([... какие-то проверки ...]) {
    throw([...]);
  }

  name = std::move(arg);
}

Создание пустой std::string (список инициализации мы же не применяем, так что пустую name придется создать до вызова тела конструктора) дешево, ибо SSO, т.е. минимальный буфер с '\0' будет с вероятностью 99% создан без применения кучи, и перемещение тоже дешево, ибо никаких реаллокаций не происходит, это просто обмен указателями - буфер, находящийся в куче, просто переезжает от одной строки к другой.

А если конструктор по умолчанию дорогой?

А если конструктор по умолчанию дорогой, то можно сделать так:

Spell(std::string_view arg)
  : name{(somePrivateStaticMethodThatPerformsNecessaryChecksAndThrows(arg), arg)}
{}

Т.е. использовать знаменитый comma operator. Это, безусловно, выглядит не очень эстетично, но работать будет.

Можно и без оператора:

Spell(std::string_view arg)
  : name{ [arg]{ checks(arg); return arg; }( ) }
{}

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

Например для того, чтобы ограничить способ создания обектов запретив создавать их на стеке, а создавать в куче или кастомным распределителем и вернуть какой нибудь std::unique_ptr/std::shared_ptr. Либо необходимо выполнять дополнительные действия до и/или после создания обекта конструктором, которые, по каким-то пичинам, нельзя выполнить в самом конструкторе - вызвать виртуальную функцию, например. Ну и т.д.

После C# очень не хватает нормального пакетного менеджера, который точно работает. Одна либа доступна только в конан, другая в vcpkg, проект, который пишу на cmake. Что делать? Это еще при том, что на windows эти менеджеры еще назло завести.

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

Ну и в целом сейчас, IMHO, нет настоящей экосистемы языка, а есть набор непонятно чего, что как-то вместе работает. Если повезёт.

В C# работа пакетного менеджера (nuget) очень сильно облегчается тем, что есть только одна (плюс-минус) система сборки с одним форматом файла (.csproj). В плюсах же, так исторически сложилось, их вагон и маленькая тележка. И каждый со своими особенностями...

Почему в стандарт нельзя было добавить формат взаимодействия компилятора и системы сборки?

А надо было всю систему сборки добавлять целиком, мне так кажется.

Не, в C# ни кто не заставляет пользоваться системой сборки msbuild(csproj -это её вотчина).

Вся простота в том, что dll сами себя описывают и распространяются в бинарном виде.

На счет добавления целиком системы сборки в стандарт - сама по себе идея мне нравится. Но комитет умеет превращать классные концепции в страшных монстров.

По-моему, основная проблема, примера с Early return - это всего лишь догматическое следование правилам стиля кода. Потому что если переписать тот же самый код таким образом

std::string applySpell(Spell* spell)
{
  	if (!spell) return "No spell";
	if (!spell->isValid()) return "Invalid spell";
	if (this->isImmuneToSpell(spell)) return "Immune to spell";
	if (this->appliedSpells.constains(spell)) return "Spell already applied";
	
	appliedSpells.append(spell);
	applyEffects(spell->getEffects());
	return "Spell applied";
}

то стандарта вполне хватает (причем - даже не стандарта, а древних ещё обычаев языка C, даже не C++, когда и стандарта-то никакого не было). Код получается не менее компактным, чем по предложениям автора статьи. И - вполне читаемым (на мой взгяд, по крайней мере, ибо читаемость кода - она, вообще-то, субъективна). Что до модифицируемости, которая, как считается, пропадает при отказе от фигурных скобок (типа, ещё один оператор так просто не добавишь), то ничто не мешает в одном нужном месте эти скобки вернуть. А если нужно добавить оператор везде, то место ему - в функции, результат которой, вызванной с прежним возвращаемым значением,.и будет возвращать в return. Случайно добавить оператор без скобок в такой код IMHO тоже не то, чтобы невозможно, но незаметно и не задумываясь - сложно.

Единственная практическая сложность, которую я вижу с этим кодом - это как настроить средства контроля стиля кода (IDE и т.п.), чтобы они не проявляли излишний фанатизм и разрешали писать код так, как удобно, а не так, как положено ;-)

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

Хотелось бы функцию сравнения двух строк без учёта регистра букв, работающую для любого языка и независимо от текущего locale. То же самое - uppercase/lowercase для строки на любом языке. Поиск в строке подстроки без учета регистра букв. И да, чтобы это всё не зависело от платформы. Бывает такое?

так эти же есть, как их? wildcard? А! регулярные выражения. Но древние бли-и-ин! Наверно побрезгуете.

В С++ нет что-ли? В библиотеках?

Сторонние библиотеки бывают, конечно. Но хотелось бы прямо в стандарте. Плюс, эти библиотеки очень немаленькие по объему кода бывают.

Регулярные выражения точно позволяют поискать подстроку на русском языке без учёта регистра букв в строке UTF8? А на французском языке? А подстроку с эмотиконами?

Я могу предположить что сделать можно так, что патерны регулярных выражений должны от  locale и от языка зависеть,

но я конечно с таким заданием из высшей математики не встречался, незнаю в общем. Любое решение проверять придется на практике, вряд ли вы найдете готовые паттерны проектирования для такой задачи, мне кажется.

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

Unicode - это очень страшно. Поэтому есть не менее страшная libicu чтобы справляться с юникодом.
Тащить юникод в стандарт - это будет больно.

Так если бы только Unicode. Есть вещи, которые в него не помещаются.

"Early return" не имеет прямого отношения к проверке граничных условий. "Early return" в компании с братом "early continue" - паттерн категории divine (т.е. применение его обязательно и эта обязательность не подлежит обсуждению), направленный на выделение и ранее отсечение простых ситуаций в рамках более сложной логики:

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

Тщательное следование этому правилу существенно повышает удобочитаемость кода.

Вы же почему-то "удавили" этот паттерн до проверки граничных условий на аргументах... Это - дискредитация идеи.

А то, что это все в вашем коде выглядит громоздко, является следствием [явно нарочитого] применения фейкового "очень полезного правила" (c), т.е. фактически антипаттерна, "всегда заключай ветки if в скобки, даже если это не нужно"

Очень интересная и познавательная статья.

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

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

Понравилась конструкция do {} while(0); в макросе.

Хотелось бы чаще встречать подобные статьи.

PS. И картинка в заголовке вообще супер.

"Понравилась конструкция do {} while(0); в макросе" - это, наверное, одна из самых старых профессиональных шуток в стиле "рука-лицо" в мире программирования на С.

Весь смысл, вся цель, всё назначение конструкции do {} while(0) в макросах сводится к тому, чтобы не включать в нее эту замыкающую ;. Именно ради этого такая конструкция и используется в макросах. Вся идея держится на уникальном свойстве синтаксиса do/while - это "операторная скобка", которая требует замыкающей точки с запятой в конце. И вся суть тут в том, что ставиться эта точка с запятой будет пользователем снаружи макроса, а не внутри. (Можно предложить и другие варианты таких "операторных скобок", но они хуже, чем do/while).

Поэтому когда вам "нравится конструкция do {} while(0); в макросе" (именно в таком виде), вы либо ничего не поняли, либо как-то очень тонко троллите...

Именно такие знания и отличают профессионала от любителя. В моей любительской практике я крайне редко сталкиваюсь с макросами и не обратил внимания на факт наличия или отсутствия ';'

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

Так что спасибо, за то что обратили мое внимание на существенный факт написания заготовки для функции в в обертке do{}while(0)

Подскажите, какие будут подводные камни от просто ; после макроса, без do/while. noop оптимизирующий компилятор удалит, не понимаю разницы между ; и ;;;;;. if(...){...}; без проблем компилируется. Разве что в режиме /wall может ругаться на лишние ;

Мне не понятно, что именно вам непонятно.

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

"Составной" в данном случае значит, что макрос не может состоять из единственного выражения: так сложилось, что он должен включать несколько statements. Например, нам внутри зачем-то нужно объявить переменную.

"Не отличалось от использования обычной функции" означает, что, в частности, мы должны иметь возможность писать, например, такой код

if (условие)
  foo(123);
else
  foo(456);


Предложите ваши альтернативные варианты. Как вы будете реализовывать такой макрос без do while?

(Сразу скажу, что альтернативные варианты существуют, но do while, пожалуй, самый естественный из них.)

Всё, я вижу свою ошибку. Если просто в фигурные скобки взять, без do-while, то развернётся в некомпилируемый код. Я забыл, что можно без скобок писать. В коде со скобками побочек бы не было, а без скобок разворачивается в некомпилируемое вот это.

if( ... )
{
 ...
};
else
{
...
};

Sign up to leave a comment.

Articles