Фичи, которых нет
Уже более десяти лет я профессионально занимаюсь C++ разработкой. Я вошел в профессию 2013 году, в самый момент, когда комитет по стандартизации языка C++ раскочегарился и встал на рельсы трехлетних релизов обновленных стандартов языка. Уже был выпущен C++11, в котором была введена куча самых заманчивых новшеств, существенно освеживших язык. Однако, далеко не каждому была доступна роскошь использовать все эти нововведения в рабочем коде, и приходилось сидеть на унылом C++03, облизываясь на новый стандарт.
Вместе с тем, несмотря на все разнообразие новых фич, внедряющихся в язык, я от проекта к проекту наблюдал и поныне наблюдаю одну и ту же повторяющуюся картину: helper-файлы, helper-контейнеры, в которых зачастую реализуются одни и те же вещи, восполняющие то, чего нет в STL. Я не говорю о каких-то узкоспециализированных специфических структурах и алгоритмах — скорее о вещах, без которых не получается комфортно разрабатывать программный продукт на C++. И я вижу, как разные компании на различных проектах сооружают одни и те же самопальные решения, просто потому что они естественны, и на них есть спрос. А предложение отсутствует, по крайней мере в STL.
В статье я хотел собрать самые яркие примеры того, что видел и использовал в разработке. Но в процессе сбора всех отсутствующих из коробки в C++ фич, внезапно для себя обнаружил, что часть из них уже покрыта новыми стандартами языка, полностью или частично. Поэтому данная статья — скорее некая рефлексия и книга жалоб о том, чего не было очень долго, но оно в итоге пришло в язык; и о том, что все еще отсутствует в стандарте. Статья не претендует ни на что, скорее просто поболтать о повседневном C++.
DISCLAIMER: в статье я могу взаимозаменять (а может быть и уже успел взаимозаменить) понятия C++, STL, язык, стандарт языка и т.п. так как в контексте статьи это не так важно, и речь будет идти "обо всем об этом".
Чего не было очень долго
std::string::starts_with, std::string::ends_with
Фантомная боль каждого второго плюсовика. Этих вещей ждали так долго, а они так долго не приходили к нам. Ставь лайк, если видел что-то похожее в закромах кодовой базы своего рабочего проекта:
inline bool starts_with(const std::string &s1, const std::string &s2)
{
return s2.size() <= s1.size() && s1.compare(0, s2.size(), s2) == 0;
}
Эти методы ввели в язык лишь C++20, который и сейчас-то далеко не всем доступен. Но счастливчики наконец-то могут найти префикс у строки. И постфикс тоже:
std::string s("c++20");
bool res1 = s.starts_with("c++"); // true
bool res2 = s.starts_with("c#"); // false
bool res3 = s.ends_with("20"); // true
bool res4 = s.ends_with("27"); // false
std::optional
Этот класс давно есть в языке, дед, иди пей таблетки — скажете вы, и будете частично правы, ведь std::optional
с нами с 17 стандарта, и все к нему прикипели как к родному. Но тут скорее моя личная боль, когда я в самые первые годы работы сидел на проекте с ограничением в стандарт C++03 и использовал самописный optional
, созданный моим коллегой.
Чтение кода, реализующего этот самописный optional
было для меня захватывающим чтивом. Я тогда был еще джуном, и на меня это сумело произвести впечатление. Да, там все было достаточно просто и прямолинейно, но эмоций было столько, будто я читаю исходники STL.
Я рад, что теперь могу писать смело и без стеснения вот так практически на любом проекте:
std::optional<Result> getResult();
const auto res = getResult();
if (res) {
std::cout << *res << std::endl;
} else {
std::cout << "No result!" << std::endl;
}
std::expected
Если вы знакомы с Rust, вы знаете, что у класса Option<T>
есть близкий соратник — класс Result<T, E>
. Они очень тесно связаны и каждый имеет пачку методов, преобразующих одно в другое.
Если с Option<T>
все понятно — это аналог optional<T>
в C++ — то с Result<T, E>
стоит пояснить. Это что-то типа optional<T>
, но отсутствие результата трактуется как ошибка типа E
. Т.е. объект класса Result<T, E>
может находиться в двух состояниях:
Состояние Ok. Тогда объект хранит в себе валидное значение типа
T
Состояние Error. Тогда объект хранит в себе ошибку типа
E
Мы всегда можем спросить объект, в каком из двух состояний он находится и попытаться взять у него валидное значение либо спросить, какая у него ошибка.
Для C++ программиста такой класс может показаться чем-то диковинным, но в Rust он имеет большое значение, поскольку в языке нет исключений, и обработка нештатных ситуаций происходит исключительно через возврат кодов ошибок и в 99% случаев это делается через возврат результата в виде объекта Result<T, E>
.
С другой стороны, я за время работы с C++ принимал участие только в проектах, где исключения были под запретом по тем или иным причинам, а в таком прочтении C++ становится аналогичен Rust в плане работы с ошибками в программе.
Именно поэтому, единожды увидев Result<T, E>
в Rust, я не смог его развидеть и завидовал Rust'у за то, что в нем Result<T, E>
есть, а в C++ его нет. И да, я написал аналог Result<T, E>
для C++. У класса было сомнительное название Maybe<T, E>
, которое могло бы ввести Haskel-программистов в заблуждение (в Haskell Maybe
— это аналог optional
)
А буквально недавно я обнаружил, что комитет по стандартизации языка C++ утвердил класс std::expected<T, E>
в 23 стандарте, и MSVC даже успели реализовать его в VS 2022 17.3 и он доступен при включении опции /std:c++latest
компилятора. И даже название вышло хорошим. На мой вкус куда лучше, чем Result или Maybe.
Оценить класс в действии предлагаю кодом, который парсит человекочитаемый шахматный адрес в координаты, которыми проще распоряжаться внутри шахматного движка. Например, "a3"
должен стать координатами [2; 0]
:
struct ChessPosition
{
int row; // stored as [0; 7], represents [1; 8]
int col; // stored as [0; 7], represents [a; h]
};
enum class ParseError
{
InvalidAddressLength,
InvalidRow,
InvalidColumn
};
auto parseChessPosition(std::string_view address) -> std::expected<ChessPosition, ParseError>
{
if (address.size() != 2) {
return std::unexpected(ParseError::InvalidAddressLength);
}
int col = address[0] - 'a';
int row = address[1] - '1';
if (col < 0 || col > 7) {
return std::unexpected(ParseError::InvalidColumn);
}
if (row < 0 || row > 7) {
return std::unexpected(ParseError::InvalidRow);
}
return ChessPosition{ row, col };
}
...
auto res1 = parseChessPosition("e2"); // [1; 4]
auto res2 = parseChessPosition("e4"); // [3; 4]
auto res3 = parseChessPosition("g9"); // InvalidRow
auto res4 = parseChessPosition("x3"); // InvalidColumn
auto res5 = parseChessPosition("e25"); // InvalidAddressLength
std::bit_cast
Это то, обо что я эпизодически спотыкался. Уж не знаю почему, но у меня периодически возникала необходимость делать странные вещи вроде получения битового представления float
числа. Конечно же в джуновские времена я не боялся UB и пользовался тем, что просто работает, по крайней мере здесь и сейчас. Итак, что у нас есть из небезопасного битового представления одного типа в другой:
-
reinterpret_cast
, куда без него. Так просто и заманчиво написатьuint32_t i = *reinterpret_cast<uint32_t*>(&f);
и не заботиться ни о чем. Но это UB;
-
Назад к корням - c-style cast. Все то же самое, что с
reinterpret_cast
, только еще проще в написании:uint32_t i = *(uint32_t*)&f;
Ведь если разработчики Quake III не чурались, то почему нельзя нам? Но.. это UB;
-
Трюк с
union
:union { float f; uint32_t i; } value32;
Сам по себе такой код не UB, но беда в том, что чтение из union-поля, в которое вы перед этим ничего не писали — это тоже UB.
Тем не менее я наблюдал все эти подходы в разных типах извращений:
Попытка узнать знак
float
числа через прочтение его старшего битаПревращение указателя в число и обратно, привет embedded. Видел экзотический случай, когда адрес превращали в ID
Математические извращения с экспонентой или мантиссой
float
Да кому и зачем нужна мантисса, спросите вы? А я отвечу: вот мой древний GitHub-проект, где я по фану сделал маленький IEEE 754 конвертер, в котором можно играться с битовым представлением 32-битных чисел с плавающей точкой. Я его делал очень давно в самообразовательных целях, к тому же очень хотелось украсть оформление стандартного калькулятора Windows7 и посмотреть, как у меня выйдет :)
В общем, битовые извращения то тут, то там кому-то да становятся необходимы.
Спрашивается, как извращаться безопасно? Когда я в свое время полез на StackOverflow за правдой, ответ был суров но единственен: "используйте memcpy
". Где-то там же я своровал небольшой сниппет, чтобы использовать memcpy
удобно:
template <class OUT, class IN>
inline OUT bit_cast(IN const& in)
{
static_assert(sizeof(OUT) == sizeof(IN), "source and dest must be same size");
static_assert(std::is_trivially_copyable<OUT>::value, "destination type must be trivially copyable.");
static_assert(std::is_trivially_copyable<IN>::value, "source type must be trivially copyable");
OUT out;
memcpy(&out, &in, sizeof(out));
return out;
}
В C++20 ввели std::bit_cast
, который делает все тоже самое за исключением того факта, что он при всем при этом еще и constexpr
благодаря магии, которую стандарт возложил на компиляторы, которым это нужно реализовывать.
Теперь мы можем прикоснуться к прекрасному и сделать его не только прекрасным, но и корректным с точки зрения спецификации языка:
float q_rsqrt(float number)
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = std::bit_cast<long>(y); // evil floating point bit level hacking
i = 0x5f3759df - (i >> 1); // what the fuck?
y = std::bit_cast<float>(i);
y = y * (threehalfs - (x2 * y * y)); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
Не благодарите, id Software.
Чего нет и может быть не будет
Математика float-чисел
Все мы знаем, что нельзя просто так взять и проверить на равенство два float
числа. 1.0
и 0.999999999
не будут равны друг другу, даже если по вашим меркам они вполне себе равны. Стандартных методов адекватного решения этой проблемы в языке нет — ты должен сам ручками сравнить модуль разницы чисел с эпсилоном.
Другая вещь, которую иногда хочется иметь под руками — округлить число до какого-то количества знаков после запятой. У нас в распоряжении есть floor
, есть ceil
, есть round
, но все они не про то, все они округляют до целого. Поэтому приходится идти на StackOverflow и брать какие-то заготовленные решения.
В итоге ваша кодовая база обрастает примерно такими хелперами:
template<class T>
bool almostEqual(T x, T y)
{
return std::abs(x - y) < std::numeric_limits<T>::epsilon();
}
template<class T>
bool nearToZero(T x)
{
return std::abs(x) < std::numeric_limits<T>::epsilon();
}
template<class T>
T roundTo(T x, uint8_t digitsAfterPoint)
{
const uint32_t delim = std::pow(10, digitsAfterPoint);
return std::round(x * delim) / delim;
}
Что тут еще можно сказать — не критично, но грустно.
EnumArray
Представим, у вас есть перечисление:
enum class Unit
{
Grams,
Meters,
Liters,
Items
};
Довольно распространенный случай, когда вам нужен словарь с enum-ключом, в котором будет храниться какой-то конфиг или просто информация о каждом элементе перечисления. В моей работе такой случай встречается часто. Первое решение в лоб легко реализуется стандартными средствами stl:
std::unordered_map<Unit, const char*> unitNames {
{ Unit::Grams, "g" },
{ Unit::Meters, "m" },
{ Unit::Liters, "l" },
{ Unit::Items, "pcs" },
};
Что мы можем подметить про этот кусок кода:
std::unordered_map
— не самый тривиальный контейнер. И не самый оптимальный по части представления в памяти;Подобного рода словари-конфиги могут встречаться в проекте ну очень часто и в подавляющем большинстве случаев они будут малого размера, ведь среднестатистическое количество элементов в перечислении редко превышает несколько десятков, а чаще всего и вовсе исчисляется штуками. Хэш-таблица, если мы используем
std::unordered_map
, или дерево, если мы используемstd::map
, начинают выглядеть как оверкилл;Перечисление по своей сути — число. Очень заманчиво представить его как числовой индекс
Последний факт может быстро привести нас к идее, что тут можно было бы сделать такой контейнер, который интерфейсно бы представлял из себя словарь, но под капотом у него лежал бы std::array
. Индексы такого массива — это элементы нашего перечисления, данные массива — значения "мапы".
Остается лишь довести до ума, как массиву дать понять, какой он должен быть длины. Т.е. как посчитать количество элементов в перечислении. Самый простой дедовский метод — добавить в конец перечисление служебный элемент Count
. На этом способе и остановимся, т.к. он не особо экзотический — я его часто вижу в кодовых базах — а значит, воспользоваться им не зазорно:
enum class Unit
{
Grams,
Meters,
Liters,
Items,
Count
};
Дальнейшая реализация контейнера достаточно проста:
template<typename Enum, typename T>
class EnumArray
{
public:
EnumArray(std::initializer_list<std::pair<Enum, T>>&& values);
T& operator[](Enum key);
const T& operator[](Enum key) const;
private:
static constexpr size_t N = std::to_underlying(Enum::Count);
std::array<T, N> data;
};
Конструктор с std::initializer_list
нужен, чтобы можно было сформировать наш конфиг точно так же как мы формировали в свое время std::unordered_map
:
EnumArray<Unit, const char*> unitNames {
{ Unit::Grams, "g" },
{ Unit::Meters, "m" },
{ Unit::Liters, "l" },
{ Unit::Items, "pcs" },
};
std::cout << unitNames[Unit::Items] << std::endl; // выведет "psc"
Красота!
В чем выражается красота:
Мы используем все прелести
std::array
иstd::unordered_map
одновременно. Удобство интерфейса словаря + быстрота и примитивность массива (в хорошем смысле) под капотом;Сache-friendly — данные лежат в памяти последовательно, совершенно не в пример
std::unordered_map
иstd::map
;Размер массива известен на этапе компиляции, а если доводить контейнер до ума, практически все его методы можно легко сделать
constexpr
.
Какие этот подход имеет ограничения:
Обязательный
Count
у перечисления;-
Перечисление не может иметь кастомных значений типа:
enum class Type { A = 4, B = 12, C = 518, D }
Только дефолтный порядок с нуля;
В массиве выделена память под все элементы перечисления сразу. Если вы заполнили
EnumArray
не всеми значениями, остальные будут содержать в себе default-constructed объекты;А это кстати еще одно ограничение — тип
T
должен быть default-constructed.
Я обычно с такими ограничениями ок, поэтому я обычно пользуюсь этим контейнером без каких-то особых проблем.
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";
}
Согласны? Узнали? Несчастные три строчки внизу — реальная работа метода. Остальное — проверки, можно ли совершить эту работу. Немного раздражает. Особенно, если вы приверженец Allman style и каждая ваша фигурная скобочка умеет выстраивать личные границы.
Хотелось бы лаконичнее, без бойлерплейта. Есть же у C++ assert
, например, который по духу похож на то, чем мы здесь занимаемся — делается проверка некоторого условия, если надо, под капотом предпринимаются меры. Правда ассерту проще — ему не нужно ничего возвращать. Но тем не менее что-то похожее мы могли бы соорудить:
#define early_return(cond, ret) \
do { \
if (static_cast<bool>(cond)) \
{ \
return ret; \
} \
} while (0)
#define early_return_void(cond) \
do { \
if (static_cast<bool>(cond)) \
{ \
return; \
} \
} while (0)
FFFUUU, макросы! Бьёрн Страуструп не любит макросы. Если он напишет мне в личку и попросит извиниться, я его пойму и извинюсь, я тоже не люблю C++ макросы.
Но да, в предлагаемом коде макросы, даже два. На самом деле мы можем сократить их до одного, если задействуем variadic macro:
#define early_return(cond, ...) \
do { \
if (static_cast<bool>(cond)) \
{ \
return __VA_ARGS__; \
} \
} while (0)
Макрос остался один, но он все еще макрос. И нет, чуда скорее всего не произойдет, его нельзя переделать в немакрос — как только мы попытаемся утащить его в функцию, мы потеряем возможность влиять на control flow нашей текущей функции. Жаль, но реальность такова. Зато посмотрите, как мы можем переписать наш пример:
std::string applySpell(Spell* spell)
{
early_return(!spell, "No spell");
early_return(!spell->isValid(), "Invalid spell");
early_return(this->isImmuneToSpell(spell), "Immune to spell");
early_return(this->appliedSpells.constains(spell), "Spell already applied");
appliedSpells.append(spell);
applyEffects(spell->getEffects());
return "Spell applied";
}
Это будет работать и в случае если бы функция возвращала void
:
void applySpell(Spell* spell)
{
early_return(!spell);
early_return(!spell->isValid());
early_return(this->isImmuneToSpell(spell));
early_return(this->appliedSpells.constains(spell));
appliedSpells.append(spell);
applyEffects(spell->getEffects());
}
Стало короче, и я считаю, что в целом стало лучше. Если бы стандарт поддерживал эту фичу, она могла бы быть уже не макросом, а полноценной языковой конструкцией. Хотя, ради забавы скажу, что плюсовый assert
— это таки тоже макрос :)
Если же вы такой строгий приверженец поведения assert
, что считаете, что условия должны работать как в assert
— утверждать ожидаемое, срабатывать при обратном — то мы можем достаточно легко удовлетворить и ваш запрос просто инвертировав всю логику и назвав макрос сообразно новому поведению:
#define ensure_or_return(cond, ...) \
do { \
if (!static_cast<bool>(cond)) \
{ \
return __VA_ARGS__; \
} \
} while (0)
void applySpell(Spell* spell)
{
ensure_or_return(spell);
ensure_or_return(spell->isValid());
ensure_or_return(!this->isImmuneToSpell(spell));
ensure_or_return(!this->appliedSpells.constains(spell));
appliedSpells.append(spell);
applyEffects(spell->getEffects());
}
Нейминг, скорее всего, неудачный, но вы уловили идею. А я был бы рад видеть в C++ любую из конструкций.
Unordered erase
Полагаю, самая часто используемая коллекция в C++ — это vector
. И все мы хорошо помним, что вектор хорош всем, кроме вставки и удаления в произвольном месте коллекции. Это занимает O(n) времени, поэтому мне каждый раз грустно что-то удалять из середины вектора, поскольку вектору придется перелопачивать половину своего контента, чтобы сместиться немного влево.
Есть идиоматичный прием, который может превратить O(n) в O(1) ценой несохранения порядка элементов в векторе. И если вы готовы заплатить эту цену, вам определенно выгоднее использовать этот несложный трюк:
std::vector<int> v {
17, -2, 1084, 1, 17, 40, -11
};
// удаляем число 1 из вектора
std::swap(v[3], v.back());
v.pop_back();
// получаем [17, -2, 1084, -11, 17, 40]
Что мы сделали? Мы сначала обменяли последний элемент вектора с помеченным на удаление, а затем просто выкинули хвостовой элемент из вектора. Обе операции очень дешевы. Просто, красиво.
Почему интерфейс вектора не располагает такой простой альтернативой обычному методу erase
, не понятно. В Rust вот, например, он есть.
Ну а нам придется заиметь в своей кодовой базе свою функцию-хелпер:
template<typename T>
void unorderedErase(std::vector<T>& v, int index)
{
std::swap(v[index], v.back());
v.pop_back();
}
Итоги
Половину статьи C++ переиграл и уничтожил еще в процессе ее написания, потому что современные стандарты C++20 и C++23 покрыли добрую половину хотелок, описанных в этой жалобной книге. В остальном же список пожеланий у пользователей языка все равно никогда не иссякнет, потому что сколько людей, столько хотелок, и все их в стандартную библиотеку или в сам язык не упихнешь.
Я постарался упомянуть только те моменты, которые на мой взгляд менее всего пахнут вкусовщиной, и были бы достойны вхождения в стандарт языка, по крайней мере в моей работе они востребованы +/- каждодневно. Вы справедливо можете иметь другое мнение на мой список, а я в свою очередь с удовольствием бы почитал в комментариях вашу боль и ваши недополученные фичи, чтобы увидеть, как пользователи языка хотели бы видеть будущее C++.
Комментарии (164)
Kelbon
15.01.2024 16:33-2Чёто странное, то опшнл у вас в С++11, то пример кода с опнлом, который вообще-то не работает (разыменование забыто), потом какие-то макросы, которые заменяются if (cond) return x;
казалось бы куда уж легче, нет, зачем-то надо спрятать контрол флоу под макрос человеку, что за "контейнер" из енама я вообще не понял, сделайте уже свич (можно макросом)AskePit Автор
15.01.2024 16:33Нда, с
std::optional
вышла ошибка, спасибо, что указали, я поправил.Я был так уверен, что четко помню, когда появился
std::optional
, что даже не удосужился ничего проверить
m0xf
15.01.2024 16:33+8Не хватает switch constexpr и for constexpr. Ну и конечно, очень жду рефлексию.
alexac
15.01.2024 16:33+4По поводу 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, было бы прекрасно.
Kelbon
15.01.2024 16:33-1Лично мне в C++ очень хочется увидеть enum с ассоциироваными значениями, как в rust/swift и паттерн-матчинг, соответственно.
давайте не будем называть if паттерн матчингом.
visit + variant гораздо сильнее и уже есть в стандартной библиотеке
wilcot
15.01.2024 16:33+3Мне интересно, как будет выглядеть на 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!() |
Kelbon
15.01.2024 16:33-2Result<Option<...
Это уже какой-то антипаттерн.и тут даже не нужен визит, просто
if (v && *v) use(**v);
Например как будет выглядеть ошибка, если я удалю ветку с
в visit компилятор заставит обработать все случаи, но в целом это аргумент так себе, т.к. существует default и бах, уже никто ничего вам не напишет
Вообще, то что вы привели это хардкоженные в компилятор вещи, я уж промолчу что вы свой Option на расте написать не сможете, как и result, а на С++ - пожалуйста, пишите под свою задачу получше.
wilcot
15.01.2024 16:33+1Это уже какой-то антипаттерн.
Возможно, хотя встречается и не сказать что редко.
я уж промолчу что вы свой 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 это выглядит читаемо, поэтому интересно увидеть какой-нибудь такой пример.
Kelbon
15.01.2024 16:33-2Точно не смогу? Вроде смог
так вы и использовали фактически встроенный в язык Option, ниже уровнем вы не можете опустится даже с всякими unsafe::union (существование которого это какой-то абсурд), в рамках языка как раз не выйдет
Ну и visit вы не сможете написать даже для стандартного Option
visit([](auto&& x) { x.foo(); }, var);
На расте невыразимо. Про выбор на перегрузках внутри лямбды промолчу, вообще никак не сделаешь
wilcot
15.01.2024 16:33+1На расте невыразимо
Верно. Хотя можно сделать что-то альтернативное, но это все же не то, что вам надо:
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++ они различаются) есть хорошая статья.
pooqpooq
15.01.2024 16:33так вы и использовали фактически встроенный в язык Option, ниже уровнем вы не можете опустится
Называть встроенный в язык tagged union чем-то плохим, что нельзя реализовать самому, очень странно. На стандартном C++ вон не получится реализовать свои функции, и какой вывод из этого надо сделать?
Ну и visit вы не сможете написать даже для стандартного Option
Если у вас одинаковое поведение для разных членов Option, то это можно вынести в отдельный тип уровнем выше. А вообще все эти вещи отлично абстрагируются через линзы/призмы, которые на хаскеле (и, возможно, на расте) делаются легко через адекватное метапрограммирование, а не как в плюсах.
domix32
15.01.2024 16:33Это похоже на ту самую оптику?
pooqpooq
15.01.2024 16:33Да, весьма. Правда, не вижу сходу по ссылке, есть ли там что-то вроде classy lenses.
AskePit Автор
15.01.2024 16:33+2Вообще, то что вы привели это хардкоженные в компилятор вещи, я уж промолчу что вы свой Option на расте написать не сможете, как и result
Шта?
pub enum Option<T> { None, Some(T), }
pub enum Result<T, E> { Ok(T), Err(E), }
Эти реализации описаны в первых же строках документации по Option и Result, и каждый может сделать свой
MyOption
иMyResult
, если ему так вздумается. Вопрос лишь в том, зачем. И из него вытекает второй вопрос: что ваш неверный аргумент доказал бы?
lorc
15.01.2024 16:33+4Я всеми руками за функциональное программирование, но надо признать что функциональный код на С++ выглядит ужасно. Уж лучше исключениями бросаться.
feelamee
15.01.2024 16:33+2мне лично не хватает какой-то единообразности в описании интерфейсов, а еще слабых дженериков как в раст, а не сильных как в плюсах.
Да, я понимаю, что концепты более общие, чем просто описание интерфейса функций. Но мне все равно, это не так читаемо и, к тому же, это не сочетается с описанием интерфейсов для динамического полиморфизма. Это уже странно. Зачем нам два способа описать интерфейс, чтобы в компайл тайме показать компилятору что какой-то тип ему соответсвует? What?
А слабые дженерики это просто способ не стрелять себе в ногу, когда вдруг решил поменять шаблонный параметр при вызове своей функции. К тому же это еще и способ описать интерфейс типа, который функция будет использовать. А то читать код библиотек, где написано <typename T> не очень приятно.
Не хватает try, хоть и можно заменить макросом (а потом наполучать по шапке ото всех, кого можно). Не хватает единой системы сборки (даже не говорите мне, что cmake... нет, он ужасен). Не хватает функциональщины.
Ну ладно, минутка нытья по поводу C++ закончилась)Kelbon
15.01.2024 16:33-2это не сочетается с описанием интерфейсов для динамического полиморфизма
в расте тоже не сочетается, не столько потому что дженерики плохие, просто мир так устроен, что-то сделать невозможно
А слабые дженерики это просто способ не стрелять себе в ногу
Ещё это способ сделать все библиотеки хуже, все стандартные интерфейсы сложнее и опять же хуже (см. итераторы, контейнеры, сравните в С++ и в расте и при должном понимании осознаете)
Если вы хотите сочетать концепты и динамическое, то вот:
https://github.com/kelbon/AnyAny
Не генерируется автоматически из концепта конечно(потому что невозможно), но дак тайпинг (с большими чем у раста возможностями) предоставляет
feelamee
15.01.2024 16:33в расте почти сочетается и это круто. Хотя я могу ошибаться с этим.
Но с первого взгляда - пишешь <T: Sortable> и dyn Sortable. Довольно хорошо сочетается.
Не понял почему хуже будет библиотеки. Примерно реализацию что там что там я знаю. Объясните, если не затруднит?
Вашу библиотеку видел, спасибо. Мне понравилась.
Kelbon
15.01.2024 16:33+2Но с первого взгляда - пишешь
только с первого взгляда, на самом деле есть некие вещи которые можно в трейтах сделать динамическими и те, которые нельзя. Например шаблон очевидно нельзя сделать виртуальной функцией, также с перегрузками и тд. Получаются неявные ограничения, которые для новичка вообще ни разу нелогичны
Насчёт библиотек назову лишь основные вещи, которые именно из-за дженериков не могут быть реализованы хорошо -
аллокаторы у каких-то контейнеров есть, у каких-то нет, зависит исключительно от того получилось ли написать это на растовых дженериках
компараторы также
нельзя расширять/специализировать позже без изменения АПИ, например в С++ после 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 на расте написать невозможно. Там впринципе условие "или" в дженериках нереализуемо
feelamee
15.01.2024 16:33+2хм, спасибо за ответ.
Но без подробностей я мало понял. Допустим вы правы. Но есть несколько вопросов.
Зачем сортировать unordered_map? По моему это не имеет смысла с точки зрения самой идеи хэштаблицы.
А нужен ли вам способ сделать "или" в определении дженерика? Как функция должна его использовать? Написать if constexpr и вызвать что-то в зависимости от типа? Тогда я думаю вам нужно две функции.
В раст ведь есть ассоциированные типы. С ними вполне можно реализовать iterator_traits
Вообщем можно долго дискутировать что лучше: интрузивные или не... интерфейсы.
Kelbon
15.01.2024 16:33-1Зачем сортировать 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;
pooqpooq
15.01.2024 16:33+2Ещё это способ сделать все библиотеки хуже, все стандартные интерфейсы
сложнее и опять же хуже (см. итераторы, контейнеры, сравните в С++ и в
расте и при должном понимании осознаете)Раст — это не топ теории типов. Статический полиморфизм с ограничениями делает отличный, выразительный и, что самое главное, легко понимаемый код. Даже если обмазать плюсовый код концептами, то нет ни гарантий их достаточности, ни помощи в написании тела функции, соответствующей концептам.
dalerank
15.01.2024 16:33+4Помню выжимку по работе комитета для с++20: начали работу над пропозалами для переноса Boost.Process в стандарт, переделку работы с файлами и вводом/выводом, linealg, юникод, 2d графика, аудио, JIT компиляцию C++ кода. Скажите, а вот это все действительно нужно в стандарте?
Все было прекрасно до c++17, сейчас просто хочется сказать "горшочек не вари"rukhi7
15.01.2024 16:33-3Мне все больше кажется что начиная с c++17 это уже не С++, это пародия на Java с указателями и с хидер-файлами.
Если без восторга прочитать статью, она о чем:
как нам не хватало функции для работы со строками,
конструкции для работы с enum -ами
конструкции для работы с битовыми представлениями
early_return тот еще шедевр, как мы без него жили раньше, срочно переписывайте мегабайты кода
std::expected std::optional - вау! еще пару темплейтов добавили в библиотеку, визжим от восторга, как мы без них жили!
Это вот прям именно то для чего создавался С++, извините но по моему это какое-то развитие в зад...
ницу, опечатка, что-то про пятницу не получилось написать.
ZirakZigil
15.01.2024 16:33начали работу над пропозалами
В такой формулирвоке это значит только то, что их рассматривают, а не то, что их готовят для ввода в стандарт.
mentin
15.01.2024 16:33+3Lifehack: не ждать когда всё добавят в стандарт, и не изобретать свои костыли, а взять качественные промышленные костыли сделанные кем-то большим. Скажем absl от Гугла, или folly от Фейсбук. Там многое из перечисленного было давно добавлено, и много такого небольшого, но полезного, что нужно очень часто. Плюс они в standards committee и их костыли часто оказываются в какой-то версии стандарта уже в std.
Vitter
15.01.2024 16:33+2Чтоб быть точным, язык 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;
AnthonyMikh
15.01.2024 16:33+3Насколько мне известно, Rust создавался под впечатлением (в частности) от OCaml непосредственно, и на OCaml даже была написана первая версия компилятора. Зачем вы его называете "старшим ML-братом Haskell" - неясно, в обоих языках есть фичи, которых нет в другом.
Melirius
15.01.2024 16:33+1Пример с биткастом будет работать, только если sizeof(float) = sizeof(long), что не гарантировано стандартом и не взлетит на Linux :) Вообще стандарт очень мало чего гарантирует, даже то, что float 32-битный, и то не факт.
pooqpooq
15.01.2024 16:33+1Зато если он не будет работать, вы узнаете об этом в компилтайме даже не в констекспр-контекстах, а это хорошо.
cortl
15.01.2024 16:33-3uint32_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++.
pooqpooq
15.01.2024 16:33+7Если f это float f; и если это не какая-нибудь экзотическая архитектура, то конкретно в этих строках UB нет.
Есть. В C++ type punning не через char и пару других типов — это UB.
Само по себе чтение из неинициализированной переменной не является UB.
А чтение из неактивного члена union'а — является.
NN1
15.01.2024 16:33Добавлю 5 копеек
https://gist.github.com/shafik/848ae25ee209f698763cffee272a58f8
cortl
15.01.2024 16:33По вашей ссылке куча примеров отстрела ноги (при чём не всегда отстрела), но она не является релевантной к прокомментированным мной кускам кода приведённых автором статьи, т.к. описивыет использование значений. В том, что прокомментировал я значения не используются. А существуют варианты использования этих значений эффективно и безопасно.
Если вы хотите обсудить конкретный пример (там их много) по вашей ссылке, укажите на него.
KanuTaH
15.01.2024 16:33По вашей ссылке куча примеров отстрела ноги (при чём не всегда отстрела), но она не является релевантной к прокомментированным мной кускам кода приведённых автором статьи, т.к. описивыет использование значений. В том, что прокомментировал я значения не используются. А существуют варианты использования этих значений эффективно и безопасно.
Пожалуйста, ознакомьтесь с правилами использования
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.
Вне зависимости от того, как вы собираетесь использовать полученное значение.cortl
15.01.2024 16:33which implies that if
T2
's alignment requirement is not stricter thanT1
'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 должен работать нормально.
KanuTaH
15.01.2024 16:33+1Ещё более жёсткий пример, чем 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 здесь.
cortl
15.01.2024 16:33-2Это как раз хрестоматийный пример 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. А может он хочет узнать, что было на стеке в данном месте за мгновение до вызова и он это с некоторой вероятностью узнает.
pooqpooq
15.01.2024 16:33+1Может быть разработчик хочет получить случайное число в этом случае, а вы утверждаете, что это UB. А может он хочет узнать, что было на стеке в данном месте за мгновение до вызова и он это с некоторой вероятностью узнает.
Разработчик может хотеть что угодно, но стандарт говорит, что так нельзя.
cortl
15.01.2024 16:33Дайте ссылку на стандарт, где написано, что такой алгоритм недопустим.
cortl
15.01.2024 16:33-1Этот пример про другое
int f(bool b) { unsigned char c; unsigned char d = c; int e = d; return b ? d : 0; }
В нём даже разные длины. И внём про кастование, а в Uninitialized scalar про мусор.
ZirakZigil
15.01.2024 16:33Пример про другое, но можно и сам пункт прочитать:
If an indeterminate value is produced by an evaluation, the behavior is undefined except in the following
casesreturn a; — тут и происходит то самое evaluation.
KanuTaH
15.01.2024 16:33+1Вы уже не в первый раз не способны прочесть текст по ссылке, отсюда и весь тот бред, который вы несёте.
cortl
15.01.2024 16:33-2std::size_t f(int x)
Было бы говорящее название у функции и всё было бы нормально.
if (x) // either x nonzero or UB
Чтобы не быть голословным в таких утвержениях нужно приводить включающий код и навешивать ярлык UB там, где он проявляется, а не где попало. А так любое if (x) становится UB.
cortl
15.01.2024 16:33-1Так тут же не происходит разыменования указателей.
Представьте, что вы кладовщик. И у вас на складе есть коробки, которые могут содержать 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?
С объединениями получится элегантнее.
KanuTaH
15.01.2024 16:33+6Вы поймите, что эти ваши аналогии про кладовщиков и прочие фантазии не имеют никакого отношения к суровой реальности, в которой компилятор имеет право выполнять любые оптимизации в расчёте на то, что вы не делаете ничего подобного тому, что вы привыкли, по-видимому, делать. То что это сейчас у вас по случайному совпадению работает без специальных ключей компилятора типа
-fno-strict-aliasing
- не более чем счастливое для вас совпадение. Сейчас работает, а в следующей версии компилятора или в немного другом случае уже не работает. Ваши фантазии глупые и вредные, но это вам ещё даст по башке, и не раз. Нет смысла тратить на вас время.
cortl
15.01.2024 16:33А чтение из неактивного члена union'а — является.
Укажите на строку в которой UB:
union { float f; uint32_t i; } value32; float f = value32.f; uint32_t i = value32.i;
pooqpooq
15.01.2024 16:33+1Обе две последних. [basic.life] в стандарте говорит, что лайфтайм объектов начинается в случае юнионов только тогда, когда вы их инициализируете списком инициализации [dcl.init.aggr], в них пишете или делаете placement new [class.union], либо в ещё паре нерелевантных случаев. На двух последних строчках справа от знака равенства происходит lvalue-to-rvalue conversion, а [conv.lval] вместе с [basic.life]/7.1 требует, чтобы лайфтайм конвертируемого объекта уже действовал, чего здесь не происходит.
cortl
15.01.2024 16:33-2Да? А я думал, что место в памяти выделяется в строке:
} value32;
и дальше мы можем делать сней что угодно.
pooqpooq
15.01.2024 16:33+1В C — возможно, я не знаю C. Но в C++ место в памяти и время жизни объекта — это две разные сущности, которые не обязаны совпадать (и совпадают далеко не всегда).
cortl
15.01.2024 16:33Мы всё ещё про фундаментальные типы говорим?
ZirakZigil
15.01.2024 16:33В том числе.
char a[sizeof(int)]{ }; // в массиве лежат объекты типа char auto iptr = new (a) int(5); // теперь там лежит int auto cptr = new (a) char('c'); // время жизни *iptr закончилось
NN1
15.01.2024 16:33+1cortl
15.01.2024 16:33Ответ тот же:
По вашей ссылке куча примеров отстрела ноги (при чём не всегда отстрела), но она не является релевантной к прокомментированным мной кускам кода приведённых автором статьи, т.к. описивыет использование значений. В том, что прокомментировал я значения не используются. А существуют варианты использования этих значений эффективно и безопасно.
Если вы хотите обсудить конкретный пример (там их много) по вашей ссылке, укажите на него.
onets
15.01.2024 16:33С enum count шикарно - но это же жесть - столько времени тратить на все эти обвязки.
Panzerschrek
15.01.2024 16:33+4Забыли преобразование enum в строку и наоборот. Каждый до сих пор пилит свои велосипеды для этого.
Panzerschrek
15.01.2024 16:33Ещё бесит, когда надо получить массив без дубликатов. Сейчас это делается вот так:
std::sort(v.begin(), v.end()); v.erase(std::unique(v.begin(), v.end()), v.end());
Это весьма многословно и хуже того подвержено ошибкам. Хотелось бы для этого иметь какой-либо стандартный метод.
pooqpooq
15.01.2024 16:33Ещё хуже, что это O(nlogn), хотя для дедупликации достаточно O(n) (пусть и ценой O(k), k — число уникальных элементов, памяти, но на что обычно плевать).
Panzerschrek
15.01.2024 16:33Ну я имел в виду скорее не просто дедупликацию, а именно нормализацию - где упорядочивание тоже необходимо. Часто такое нужно, когда vector используют как set для небольшого количества элементов. Тогда строгий порядок и отсутствие дубликатов важны, чтобы корректно работало сравнение.
sergio_nsk
15.01.2024 16:33-3Так и не выбрался из джунов. Вот таким должно быть сравнение начала
inline bool starts_with(const std::string &s1, const std::string &s2) { return !s1.rfind(s2, 0); }
Kelbon
15.01.2024 16:33Это шутка какая-то? Сделал полностью нерабочее решение, которое ещё и делает поиск внутри всей строки, вместо только начала
https://godbolt.org/z/nsns843Ws
P.S. зачем вы сделали "заумно", вместо haystack.find(needle) == 0 ?
(не делайте так, это поиск по всей строке вместо сравнения только в начале)sergio_nsk
15.01.2024 16:33-4Да, чувак, да ты полный ноль. С чего это "hello" будет начинаться с "hello world". Ты перепутал аргументы. RTFM, лошара. Где там поиск внутри всей строки назад от первого символа.
Kelbon
15.01.2024 16:33Переменные называть научись, s1 s2,
Учу читать:
starts_with(A, B)
B Начинается с Asergio_nsk
15.01.2024 16:33-1Да это извращение какое-то, никто так не читает. Объявление и семантика функции взяты из статьи. Ты ещё не читал стандарт, там просто могут быть c и d.
MtTheDamos
15.01.2024 16:33+2Вкусовщина, но мне однажды в университете понадобились BigInteger
Дело было в 20-ом году, в других языках они давно были, а C++ до сих пор нет
Kelbon
15.01.2024 16:33в бусте есть
dalerank
15.01.2024 16:33+3А вы пробовали использовать буст в реальном проекте? Очень в редких случаях получается взять пару хедеров в проект, и не притащить туда весь буст. Ну это ладно, но со временем хедеры буста начинают оплетать проект как паутина, а время компиляции летит в космос. А хотели затащить только хедер для работы со временем
atd
15.01.2024 16:33Для того, чтобы затащить пару хедеров из буста, очень часто нужно изрядно попотеть, он там весь внутри тоже переплетён неслабо...
TIEugene
15.01.2024 16:33-4// use libstdc++-devel #include <cstdint> #define BigInteger int64_t
Не?
ZirakZigil
15.01.2024 16:33+1Хочу переменную со значением 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF. Подскажите, как её в ваш
BigInteger
поместить?TIEugene
15.01.2024 16:33-1Понятия не имею.
- "ваш" - это обращение к группе. Ну то есть не к конкретному человеку. Ну то есть ответственного нет.
- Автор термина 'BigInteger' в этом топике - MtTheDamos, что он имел ввиду под этим термином - к нему вопросы. Может int64, может uint64, может xint128, может как в питоне без ограничений, я х.е.з.UB
ZirakZigil
15.01.2024 16:33Надо было не числа просить вставлять, а сразу писать шутку про формально верный, но бесполезный ответ.
MtTheDamos
15.01.2024 16:33+1Мне нужны были числа Эйлера.
Я невнимательно прочитал задание, поэтому нашёл аж 243 числа.
А надо было только первые 30.
Если интересно, то 30-ое выглядит так:
12622019251806218719903409237287489255482341061191825594069964920041
А 243-е вот так:
1552754587364941454091550628784573284531417585974177462661665696581096507450323154161877012935634703115951325817420960313543117775844223174861285058216045137219151106252982387332536816251605356614666841757142282287222638475081488010899740219873267392580833806849633909800740090249332088318254353071347881225103802667657014130960756876086387423475394715873819555706637695073767542791670601109181667857352033359043188645495812422523272030548619985258815858118870195783453230356977669973606497306955094418814719082309467098992785483255018790754552225354691932914305234629248862187055107065031861150000572886966684462489433279684361321459069012573266036514127409836511791025455800207112508265787428351943299214579044828616228994840116871643043913618195850146815307497383497541328636894508200652455076599811968923451876201721405398725502150549795883247394980411420203229056814391742354498516096156540857112971088520588190469400457085900836559263116579625640443055308956373497319145122411468464451285605
Когда я вставил первые 243 в Visual Studio, то он крашнулся, а когда я пролистывал мимо них, то начинало лагать. Жаль не пригодились они, эпично было быTIEugene
15.01.2024 16:33C - скальпель.
C++ - скальпель с обрезиненной ручкой.
Не стоит размахивать скальпелем везде подряд куда попало.
Прежде всего Вы порежетесь.Вам нужно нечто вроде пассатижей, поэтому Java/C#/Python/PHP (пардон)/JS и прочая фигня.
Или читать документацию.PS. а еще лучше начать с Ассемблера
MtTheDamos
15.01.2024 16:33На этих лабораторных (предмет назывался "Высокопроизводительные вычисления") нельзя было выбрать язык программирования - только C++
А когда в следующий раз я работал с большими числами (это были лабораторные по шифрованию) я использовал PythonTIEugene
15.01.2024 16:33Хороший тамада. И конкурсы интересные.Хороший предмет. И лабораторные интересные.
Мне жаль, что вас ограничивают в языках. Язык подбирается под задачу.Python - быстрое макетирование. Для MVP (Minimum Viable Product)
C++ - Скорость почти как C и удобства почти как Python. Но именно почти. Для первого релиза.
C - почти Assembler. Куча мороки, но скорость почти на пределе. Но там всё ручной работы.
Assembler - ну тут понятно.
В принципе выбор C++ для "Высокопроизводительные вычисления" вполне себе. Баланс между "яжнепрограммист" и скоростью.
Но не ждите от C++ заноса хвоста на всех поворотах. Это уже не C, но от питона оно еще дальше.
eao197
15.01.2024 16:33В принципе выбор C++ для "Высокопроизводительные вычисления" вполне себе. Баланс между "яжнепрограммист" и скоростью.
Но не ждите от C++ заноса хвоста на всех поворотах. Это уже не C, но от питона оно еще дальше.
Интересно, а какие преимущества у чистого Си перед C++ в "высокопроизводительных вычислениях" кроме наличия ключевого слова
restrict
?TIEugene
15.01.2024 16:33И это был отличный вопрос.
Ответ: ни у каких ЯП ни перед какими ЯП нет никаких преимуществ и/или недостатков. У отвертки нет абстрактного тотально преимущества над молотком. У молотка надо отверткой тоже. Пассатижи курят в стороне задумчиво и думают "не дай бог...".eao197
15.01.2024 16:33Ответ: ни у каких ЯП ни перед какими ЯП нет никаких преимуществ и/или недостатков.
Ню-ню. Ню-ню.
Попробуйте поделать высокопроизводительные вычисления на языках, в которых нельзя отключить ран-тайм проверки (типа валидности индексов или переполнения чисел). И в которых программист вообще не имеет возможности воздействовать на расположение данных в памяти.
Ну и вас не смущает то факт, что ваша фраза "ни у каких ЯП ни перед какими ЯП нет никаких преимуществ и/или недостатков" тупо делит на ноль ваш же перечень языков с их характеристиками (типа Python -- это для MVP, а Assember -- "ну тут понятно")? Ведь если нет преимуществ, то подобное ранжирование бессмысленно.
domix32
15.01.2024 16:33early_return
Вот собственно поэтому нужны
expected
и допилить семантику элвис оператора человеческу, а то по сей день приходится копипастить. В идеале конечно как в расте сделать что-то вродеeval()?
который самостоятельно лифтанет результат из функции в случае ошибки, но из-за алгоритмов врядли оно появится.Почему интерфейс вектора не располагает такой простой альтернативой обычному методу
erase
, не понятно. В Rust вот, например, он есть.потому что erase-remove idiom мешает. Ну и плюс там итераторы везде, которые знаю только про себя, но не про конец. Опять же алгоритмы устроены так, что в тот же
std::remove
можно передать только кусок вектора, как из негоpop_back
ать тогда, если конец где-нибудь на середине оказался. Вот так и живём.Вообще очень недостаёт Range-based алгоритмов и их адекватного пайпинга.
Ну а касательно опционалов - если вы пишите кроссплатформенные приложения, то семанитика использования опционалов может различаться в зависимости от компилятора. Помнится там то ли
.has_value()
то ли.value_or() был на яблочном шланге, но отсутствовал в msvc компиляторе. Из-за этого пришлось уродовать большим количеством кода.
Ну а пример со Spell как обычно просит распилить это и гонять все это через ECS.
cdriper
15.01.2024 16:33+1Куча всего была в бусте сильно задолго до 13-го года.
Ну и плюсы тем и хороши, что очень многое можно написать самостоятельно без потери эффективности и выразительности.
TIEugene
15.01.2024 16:33Вы же не путаете "C++" и "C++ Standard Library", правда ведь?
atd
15.01.2024 16:33У популярных компиляторов версия языка привязана к версии стдлиб. Стандарт на язык точно содержит раздел про стыдлиб, с C++03 точно, для более ранних не уверен.
Так что можно обсуждать это всё вместе, выходят они плюс-минус одновременно, если вы конечно не пользуетесь preview-версиями компиляторов.
TIEugene
15.01.2024 16:33Автор не указал C++-что он имеет ввиду - язык, стандартную библиотеку или компилятор.
Например в том же питоне с каждой версией языка появляются новые конструкции.
Меняется именно язык.
При этом стандартная библиотека меняется отдельно. Хотя да, идет в комплекте.
Про трансляторы молчу, их не один.AskePit Автор
15.01.2024 16:33Автор специально сделал в статье disclaimer, чтобы индульгировать себя от фанатов терминологии и комментариев на эту тему. Но автор недооценивал комментаторов
TIEugene
15.01.2024 16:33Автор довольно тщательно спрятал Disclaimer, хороший.
Читатели не внимательные, не хорошие.
pooqpooq
15.01.2024 16:33+3Стандартная библиотека C++ — это часть языка C++. Стандарт C++ описывает стандартную библиотеку в том числе
TIEugene
15.01.2024 16:33-2Стандартная библиотека C++ - это часть стандарта C++.
Не языка. На синтаксис и поведение библиотеки не влияют.pooqpooq
15.01.2024 16:33+3Язык C++ — это то, что описывается стандартом C++, поэтому не вижу разницы.
Ну и на поведение, конечно, эти библиотеки влияют, потому что некоторые языковые вещи требуют поддержки библиотек (`std::initializer_list`, всякая корутиновая ерунда, некоторые реализованные компиляторными интринсиками type traits, и так далее).
TIEugene
15.01.2024 16:33-1это то, что описывается стандартом C++, поэтому не вижу разницы
Показываю разницу: общее != частное.
Стандарт описывает язык и stdlib.
Стандарт языка не включает stdlib.Для примера можете снести у себя libstd++-devel.
И следите за руками:echo "int main(void) {int a = 2*2; return 0;}" > test.cpp && gcc test.cpp
Оно собирается и работает.
Без стандартных библиотек.pooqpooq
15.01.2024 16:33+1Стандарт описывает язык и stdlib.
Стандарт языка не включает stdlib.Звучит так, будто у вас есть отдельно стандарт и отдельно стандарт языка. Это не так, есть единственный стандарт для каждой версии (вроде ISO/IEC 14882:2020), и там описывается и язык, и стандартная библиотека. Нет отдельного «стандарта языка».
Оно собирается и работает.
Очень здорово. Теперь напишите где-нибудь co_return или, зачем далеко ходить, typeid, вот прям просто
void foo() { typeid(int); }
TIEugene
15.01.2024 16:33-1Эта площадка слишком токсичная для дискуссий.
Я согласен с Вами полностью и абсолютно со всем.
Tujh
15.01.2024 16:33Перечисление не может иметь кастомных значений типа:
Если использовать сишный enum а не enum class - то можно.
alexejisma
15.01.2024 16:33Мы вынуждены использовать 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>() ); }
AnthonyMikh
15.01.2024 16:33+2Давайте посмотрим на типичную функцию с некоторым количеством проверок на пограничные состояния:
Давайте, и я вам покажу, что дело не в отсутствии ранних возвратов, а в кривой архитектуре. А чтобы разговор был более конкретным - напишу минимальный дополнительный код, чтобы он компилировался:
Оригинальный код
#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 вы меня не особо убедили.
AskePit Автор
15.01.2024 16:33+1Я, конечно, потратил некоторое время, чтобы придумать синтетический пример с большим количеством проверок на пограничные кейсы. Не сколько предендующий на грамотность, сколько на показательность в контексте данной статьи. Но вы явно приложили больше времени на борьбу с этим синтетическим примером.
В общем, оба мы здорово провели время :)
AnthonyMikh
15.01.2024 16:33+1Я, конечно, потратил некоторое время, чтобы придумать синтетический пример с большим количеством проверок на пограничные кейсы. Не сколько предендующий на грамотность, сколько на показательность в контексте данной статьи.
Более приближенный к реальности пример был бы убедительнее.
isadora-6th
15.01.2024 16:33static Spell construct(std::string_view name)
Периодически сталкиваюсь со статическим конструктором, при скрытом реальном и всегда интересно зачем это может быть нужно. Эксепшены из конструкторов кидать прям совсем моветон? Или причина какая есть?
AnthonyMikh
15.01.2024 16:33Моё C++-кунг-фу недостаточно сильно. Я не разобрался, как исполнить какой-то код до вызова делегирующего конструктора.
KanuTaH
15.01.2024 16:33А зачем? Можно же сделать так:
Spell(std::string arg) { if ([... какие-то проверки ...]) { throw([...]); } name = std::move(arg); }
Создание пустой
std::string
(список инициализации мы же не применяем, так что пустуюname
придется создать до вызова тела конструктора) дешево, ибо SSO, т.е. минимальный буфер с'\0'
будет с вероятностью 99% создан без применения кучи, и перемещение тоже дешево, ибо никаких реаллокаций не происходит, это просто обмен указателями - буфер, находящийся в куче, просто переезжает от одной строки к другой.AnthonyMikh
15.01.2024 16:33А если конструктор по умолчанию дорогой?
KanuTaH
15.01.2024 16:33А если конструктор по умолчанию дорогой, то можно сделать так:
Spell(std::string_view arg) : name{(somePrivateStaticMethodThatPerformsNecessaryChecksAndThrows(arg), arg)} {}
Т.е. использовать знаменитый comma operator. Это, безусловно, выглядит не очень эстетично, но работать будет.
ZirakZigil
15.01.2024 16:33+1Можно и без оператора:
Spell(std::string_view arg) : name{ [arg]{ checks(arg); return arg; }( ) } {}
KanuTaH
15.01.2024 16:33Можно, но при сборке с
-O0
(например, в отладочных сборках) это будет выглядеть несколько более развесисто за счет лишней прослойки. В сборках с оптимизацией, однако, разницы не будет.
naviUivan
15.01.2024 16:33Например для того, чтобы ограничить способ создания обектов запретив создавать их на стеке, а создавать в куче или кастомным распределителем и вернуть какой нибудь std::unique_ptr/std::shared_ptr. Либо необходимо выполнять дополнительные действия до и/или после создания обекта конструктором, которые, по каким-то пичинам, нельзя выполнить в самом конструкторе - вызвать виртуальную функцию, например. Ну и т.д.
buldo
15.01.2024 16:33+3После C# очень не хватает нормального пакетного менеджера, который точно работает. Одна либа доступна только в конан, другая в vcpkg, проект, который пишу на cmake. Что делать? Это еще при том, что на windows эти менеджеры еще назло завести.
Вообще иногда кажется, что комитет развивает абстрактный язык в вакууме. В итоге после добавления в стандарт модулей, возникло ощущение, что компиляторы и системы сборки вообще не понимали как это всё реализовать так, чтобы всё работало вместе. В cmake модули добавляли 5 лет. И то это потребовало чтобы компиляторы отдавали инфу о зависимостях между модулями. И clang с gcc делают это нифига не одинаковым способом. Почему в стандарт нельзя было добавить формат взаимодействия компилятора и системы сборки?
Ну и в целом сейчас, IMHO, нет настоящей экосистемы языка, а есть набор непонятно чего, что как-то вместе работает. Если повезёт.
atd
15.01.2024 16:33В C# работа пакетного менеджера (nuget) очень сильно облегчается тем, что есть только одна (плюс-минус) система сборки с одним форматом файла (.csproj). В плюсах же, так исторически сложилось, их вагон и маленькая тележка. И каждый со своими особенностями...
Почему в стандарт нельзя было добавить формат взаимодействия компилятора и системы сборки?
А надо было всю систему сборки добавлять целиком, мне так кажется.
buldo
15.01.2024 16:33Не, в C# ни кто не заставляет пользоваться системой сборки msbuild(csproj -это её вотчина).
Вся простота в том, что dll сами себя описывают и распространяются в бинарном виде.
На счет добавления целиком системы сборки в стандарт - сама по себе идея мне нравится. Но комитет умеет превращать классные концепции в страшных монстров.
mvv-rus
15.01.2024 16:33+2По-моему, основная проблема, примера с 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 и т.п.), чтобы они не проявляли излишний фанатизм и разрешали писать код так, как удобно, а не так, как положено ;-)
Sazonov
15.01.2024 16:33Тут уже много хороших комментов написали. По поводу енамов - посмотрите либу magic_enums, и получится что можно сделать то, что вы говорите что якобы нельзя :)
voldemar_d
15.01.2024 16:33Хотелось бы функцию сравнения двух строк без учёта регистра букв, работающую для любого языка и независимо от текущего locale. То же самое - uppercase/lowercase для строки на любом языке. Поиск в строке подстроки без учета регистра букв. И да, чтобы это всё не зависело от платформы. Бывает такое?
rukhi7
15.01.2024 16:33так эти же есть, как их? wildcard? А! регулярные выражения. Но древние бли-и-ин! Наверно побрезгуете.
В С++ нет что-ли? В библиотеках?
voldemar_d
15.01.2024 16:33Сторонние библиотеки бывают, конечно. Но хотелось бы прямо в стандарте. Плюс, эти библиотеки очень немаленькие по объему кода бывают.
Регулярные выражения точно позволяют поискать подстроку на русском языке без учёта регистра букв в строке UTF8? А на французском языке? А подстроку с эмотиконами?
rukhi7
15.01.2024 16:33Я могу предположить что сделать можно так, что патерны регулярных выражений должны от locale и от языка зависеть,
но я конечно с таким заданием из высшей математики не встречался, незнаю в общем. Любое решение проверять придется на практике, вряд ли вы найдете готовые паттерны проектирования для такой задачи, мне кажется.
Одно могу сказать точно: я бы точно с регулярных выражений начал проверять.
lorc
15.01.2024 16:33+2Unicode - это очень страшно. Поэтому есть не менее страшная libicu чтобы справляться с юникодом.
Тащить юникод в стандарт - это будет больно.
TheCalligrapher
15.01.2024 16:33"Early return" не имеет прямого отношения к проверке граничных условий. "Early return" в компании с братом "early continue" - паттерн категории divine (т.е. применение его обязательно и эта обязательность не подлежит обсуждению), направленный на выделение и ранее отсечение простых ситуаций в рамках более сложной логики:
В рамках реализации относительно сложной логики всегда старайтесь сразу выделить, отсечь и обработать более простые варианты, в порядке повышения сложности. Т.е. в первую очередь отсекается и выполняется самая тривиальная обработка. Завершенность обработки должна быть обязательно подчеркнута явнымreturn
(в функции) илиcontinue
(в итерации цикла)Тщательное следование этому правилу существенно повышает удобочитаемость кода.
Вы же почему-то "удавили" этот паттерн до проверки граничных условий на аргументах... Это - дискредитация идеи.
А то, что это все в вашем коде выглядит громоздко, является следствием [явно нарочитого] применения фейкового "очень полезного правила" (c), т.е. фактически антипаттерна, "всегда заключай ветки if в скобки, даже если это не нужно"
atd
Вот от себя добавлю:
almostEqual
Плюс такие же функции, где epsilon передаётся, и ещё пачка функций про NaN (eq or nan, gt or nan etc...).
И ещё хочется, чтобы были нормальные и не очень медленные делегаты в языке. Иначе приходится таскать FastDelegates, которым сто лет в обед (и куча макросов внутри), либо их более новую реинкарнацию https://www.youtube.com/watch?v=Mx_Q8LKltFs
> std::string::starts_with
а string::split не завезли...
YogMuskrat
Зато завезли std: :views: :split
domix32
А везде ли его завезли.
isadora-6th
Подскажите пожалуйста, чем отличаются делегаты от лямбды?
atd
Пройдите по ссылке и прочитайте. Если не любите читать — пройдите по второй ссылке, там видео, в обоих случаях этот вопрос объясняется.
isadora-6th
Все же посмотрел материалы, спасибо. Но таки не понял, чем function так тяжел и чем пример в толке лучше.
По ссылке фаст-делегатов, получается какая-то лямбда с экстра шагами (статья 2005 года так то) или реализация лямбды для C++ < 11.
Вот и спрашиваю, в чем конкретно разница.
Да, лямбды с захватом не кастятся к указателю на функцию, но если вы принимаете темплейт или готовы носить std::function - пожалуйста захватывайте.
Вот и интересуюсь, чем вас не устраивают лямбды или какие медленные уже есть (no offence, правда интересно)
atd
Лямбды правда медленные аж жесть, измеряли. С другой стороны, не всем нужна большая производительность/низкие задержки, но с третьей стороны, если это всё не нужно, то и C++ не нужно, для таких случаев есть куча медленных языков на выбор.
KanuTaH
Что, правда? Кто измерял и как? Вы лямбды с
std::function
не путаете случайно, это же разные вещи? Сами по себе лямбды прекрасно инлайнятся и оптимизируются, а вотstd::function
, действительно, вещь относительно медленная, поэтому для передачи лямбд лучше всего использовать шаблоны там, где это возможно (т.е. там, где их будут сразу вызывать, а не хранить).atd
Там, где можно заинлайнить, делегаты и не нужны. Они нужны, когда случается динамическая (во время выполнения) подписка/отписка на события нескольких возможных обработчиков.
KanuTaH
Ну предположим, только вывод "лямбды медленные аж жесть" отсюда никак не следует, мухи отдельно, котлеты отдельно. Вы, видимо, имеете в виду все-таки
std::function
. Опять же, медленные по сравнению с чем? С заинлайненными лямбдами - несомненно. С делегатами в C# - citation needed.NN1
Подбросить вам ?
Если что , я не согласен с доводами, что решение это указатель на функцию , но всё же std::function мог быть чуть оптимальней
C++23 привносит move_only_function для решения проблемы.
https://ricomariani.medium.com/std-function-teardown-and-discussion-a4f148929809
naviUivan
Интересно. И правда жесть? А можно посмотреть как Вы измеряли производительность лямбд?
atd
Посмотреть нет, но выяснилось, что std::function медленнее чем FastDelegates на несколько миллионов долларов.
Там, где лямбда будет заинлайнена, конечно разницы нет, более того, там никакой вид делагатов не нужен.
naviUivan
Вы все же путаете понятия. Лямбды и std::function это разные вещи. То что std::function медленнее чем некие FastDelegates, возможно, не спрою, не знаю, не сравнивал. Но лямбды тут совершенно не причем.
Лямбды в С++, это, по факту, синтаксический сахар над объектами классов с определенным методом operator()(....). А если лябда без захвата, то это фактически обычная функция на которую можно получить указатель. Что тут может быть жутко медленного? Обычные функторы С++ или просто функции (лямбда без захвата) с теми же накладными расходами, возможностями по оптимизации и производительностью.
AnSa8x
А в чём, собственно, проблема получить указатель на лямбду с захватом?
https://godbolt.org/z/ezs5Thqjr
naviUivan
Конечно не проблема. Не совсем внятно выразился, имел ввиду указатель на функцию (ведь лямбду без захвата можно рассматривать как обычную свободную функцию), а не указатель на объект некоего скрытого класса в кторый превращается лямбда с захватом.
AnSa8x
В каком же месте они "аж жесть" какие медленные и в сравнении с чем, позвольте узнать. Уж очень интересно.
voldemar_d
По своему опыту могу сказать, что вызов функции из указателя на функцию сильно быстрее, чем из переменной типа std::function. Попробуйте в отладчике по шагам посмотреть, что происходит в обоих случаях.
AnSa8x
Да причём тут указатель на функцию и std::function если речь шла про лямбды, которые неплохо встраиваются, о чём в этой теме только ленивый не написал.
rukhi7
вроде как делегат это указатель на любую функцию именованную или не-именованную(без имени), а лямбда это всегда указаель на не-именованную функцию, а может даже сама эта не-именованная функция, тут я думаю мнения разойдутся.
Еще есть интересный вопрос какому классу принадлежат лямбды, если они вообще принадлежат классу какому-то, то есть являются ли они методом какого-то класса.
ZirakZigil
Каждая лямбда принадлежит к уникальному анонимному неназываемому типу. Т.е.
Сама лямбда это не метод, но у неё есть член-operator(), который, как и у "обычного" класса, можно вызывать вручную (даже шаблонный):
rukhi7
вот вот! а в C# есть полноценные делегаты, как полноценный тип, и поэтому никакой трехомудии с темплейтами не нужно.
Как было безобразие с темплейтами:
https://habr.com/ru/articles/770116/
так и осталось
ZirakZigil
Во-первых, никакой трехомудрии тут нет, вывод типов-то не отключается, вызывайте как обычную функцию через () и будет вам счастье. Пример просто демонстрирует то, чем лямбы, по сути являются.
Не очень понимаю (ну тут, ни из статьи по ссылке), в чём проблема. Там есть добавленный вопрос в посте: "А что std::function разрешает такой синтаксис:".
Ответ: да, разрешает. В стандарте нет гайда для вывода, так придётся написать тип руками. Впрочем, гайд можно написать и самому, тогда будет именно так как вы предлагали:
DelegateType test_delegate = &test_class.Foo;
Бонусом, достаточно умный компилятор это всё ещё и заоптимизирует до mov eax, 15.
rukhi7
хотелось бы вот такой синтаксис:
Объект f1 должен сохранить в себе и указатель-смещение к функции
foo
и объекта
от которого эту функцию надо вызвать!То есть надо в синтаксист ввести новый "Тип Типов" Delegate, по аналогии и в ряду с struct и class, с помощью которого можно объявлять тип "указатель на функцию член класса(любого класса)". Я бы понял если бы это не делали потому что такую конструкцию нельзя бы было скомпилировать , но ее скомпилировать можно (моя статья про то ,что скомпилировать можно, как мне кажется, по крайней мере никто не пытался доказать обратное пока, наверно эта основная идея невнятно написана у меня в статье), потом
далее это мое личное мнение (хотя это все ИМХО мое), оно может быть и ошибочное,
но мне кажется это было бы намного удобнее чем возня с темплейтами,
и мне кажется, даже более типобезопасно было бы, так как вас заставляют явно прописать сигнатуру функции как тип, пусть особенный, но тип.
и это все также поддается оптимизации при компиляции, насколько я знаю
ZirakZigil
Я всё ещё пытаюсь понять, какую проблему мы решаем. Засуньте лямбу, захватывающую ссылку на объект, в std::function. Или вообще самой этой лямбдой пользуйтесь, никакой возни с шаблонами там нет.
rukhi7
а как же типобезопасность, например? Сигнатура лямбды никак не ограничена сигнатурой метода который мы в нее засунем.
Вобще говоря я привык, еще с доисторических времен, интерфейсами пользоваться. В этом смысле, то что мы обсуждаем для меня тоже как бы не проблема или некоторая абстрактная проблема.
Но раз вы не видите проблемы, значит я не смог ее внятно сформулировать или ее действительно нет, а я что-то просто нафантазировал, значит дискуссии не получится, жаль.
ZirakZigil
При желании можно форсить одинаковость параметров, тоже пишется один раз. И потом, чем, конкретно, это:
менее безопасно чем это:
?
Магии-то не бывает, ваш делегат будет, фактически, представлять из себя структурку, хранящую указатель на объект и указатель на код. Фактически, это и есть захватывающая лямбда. Хочется сахара? При желании ,можно написать функцию auto delegate(A &, &A::foo), которая будет такую лямбду создавать. Не думаю, что ради этого стоит аж целое ключевое слово вводить.
rukhi7
тем что в это f можно напихать чего угодно, в конце концов. И как показывает практика кто-нибудь в конце концов напихает. Вообще это похоже на какую-то червоточину, на мой сугубо субъективный взгляд.
мне кажется (может даже просто приснилось :) что если бы его ввели, то все были бы в восторге, это тоже мое чисто субъективное мнение.
Спасибо за конструктивный ответ!
ZirakZigil
А в делегат нельзя? Вы выше писали:
Что мне тут мешает написать f1 = b.bar, при условии, что типы совпадают?
rukhi7
ничего не мешает, только типы должны совпадать, я именно про проверку по типу.
А в лямбду можно любую дополнительную логику дописать, это же по сути функция чтобы вызвать функцию. Ведь лямбда это функция, только без имени, так ведь? и если кто-то добавит туда логику кроме вызова, ее уже никто не оптимизирует.
AnSa8x
Ну вот же на cppreference:
rukhi7
вот это мне пригодится, спасибо за цитату, буду знать на что ссылаться, при случае.
Kelbon
во-первых зачем они "в языке", во вторых std::function более чем работает и не "too heavy", его проблемы что нужно копирование + иногда хочется ссылку на функцию, а не значение. Это всё тоже решается и довольно легко (куча библиотек)
voldemar_d
Вызвать функцию из указателя куда быстрее, чем из std::function.