Фичи, которых нет

Уже более десяти лет я профессионально занимаюсь 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)


  1. atd
    15.01.2024 16:33
    +12

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

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

    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 не завезли...


    1. YogMuskrat
      15.01.2024 16:33
      +2

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


      1. domix32
        15.01.2024 16:33

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


    1. isadora-6th
      15.01.2024 16:33
      +2

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


      1. atd
        15.01.2024 16:33

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


        1. isadora-6th
          15.01.2024 16:33
          +1

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

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

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

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

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

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


          1. atd
            15.01.2024 16:33

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


            1. KanuTaH
              15.01.2024 16:33
              +4

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

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


              1. atd
                15.01.2024 16:33

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


                1. KanuTaH
                  15.01.2024 16:33
                  +3

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

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


              1. NN1
                15.01.2024 16:33
                +1

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

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

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

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


            1. naviUivan
              15.01.2024 16:33
              +1

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


              1. atd
                15.01.2024 16:33

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

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


                1. naviUivan
                  15.01.2024 16:33
                  +2

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


                  1. AnSa8x
                    15.01.2024 16:33

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

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


                    1. naviUivan
                      15.01.2024 16:33

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


            1. AnSa8x
              15.01.2024 16:33
              +2

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


              1. voldemar_d
                15.01.2024 16:33
                +1

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


                1. AnSa8x
                  15.01.2024 16:33
                  +2

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


      1. rukhi7
        15.01.2024 16:33

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

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


        1. ZirakZigil
          15.01.2024 16:33
          +3

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

          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


          1. rukhi7
            15.01.2024 16:33
            -2

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

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

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

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


            1. ZirakZigil
              15.01.2024 16:33

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

              Не очень понимаю (ну тут, ни из статьи по ссылке), в чём проблема. Там есть добавленный вопрос в посте: "А что 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.


              1. rukhi7
                15.01.2024 16:33
                -1

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

                //вместо std::function f1 = &A::foo;
                DelegateType f1 = a.foo;
                
                //где DelegateType надо объявить как особенный тип
                Delegate int DelegateType();

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

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

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

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

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

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


                1. ZirakZigil
                  15.01.2024 16:33

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


                  1. rukhi7
                    15.01.2024 16:33

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

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

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

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


                    1. ZirakZigil
                      15.01.2024 16:33
                      +1

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

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

                      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), которая будет такую лямбду создавать. Не думаю, что ради этого стоит аж целое ключевое слово вводить.


                      1. rukhi7
                        15.01.2024 16:33
                        -1

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

                        f =

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

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

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

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

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


                      1. ZirakZigil
                        15.01.2024 16:33

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

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

                        //вместо std::function f1 = &A::foo;
                        DelegateType f1 = a.foo;
                        
                        //где DelegateType надо объявить как особенный тип
                        Delegate int DelegateType();

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


                      1. rukhi7
                        15.01.2024 16:33

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

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


        1. AnSa8x
          15.01.2024 16:33
          +1

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

          Ну вот же на cppreference:

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


          1. rukhi7
            15.01.2024 16:33

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

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


    1. Kelbon
      15.01.2024 16:33
      -1

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

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


      1. voldemar_d
        15.01.2024 16:33

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


  1. Kelbon
    15.01.2024 16:33
    -2

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


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


    1. AskePit Автор
      15.01.2024 16:33

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

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


  1. m0xf
    15.01.2024 16:33
    +8

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


  1. 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, было бы прекрасно.


    1. Kelbon
      15.01.2024 16:33
      -1

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

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

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


      1. 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!()
            |
        


        1. Kelbon
          15.01.2024 16:33
          -2

          Result<Option<...

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

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

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


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

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

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


          1. 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 это выглядит читаемо, поэтому интересно увидеть какой-нибудь такой пример.


            1. Kelbon
              15.01.2024 16:33
              -2

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

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

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

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

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


              1. 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++ они различаются) есть хорошая статья.


              1. pooqpooq
                15.01.2024 16:33

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

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

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

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


                1. domix32
                  15.01.2024 16:33

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


                  1. pooqpooq
                    15.01.2024 16:33

                    Да, весьма. Правда, не вижу сходу по ссылке, есть ли там что-то вроде classy lenses.


                    1. domix32
                      15.01.2024 16:33

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


          1. 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, если ему так вздумается. Вопрос лишь в том, зачем. И из него вытекает второй вопрос: что ваш неверный аргумент доказал бы?


      1. domix32
        15.01.2024 16:33
        +1

        В плюсы есть пропозал на нормальный паттер-матчинг.


    1. lorc
      15.01.2024 16:33
      +4

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


  1. feelamee
    15.01.2024 16:33
    +2

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

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

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

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

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


    1. Kelbon
      15.01.2024 16:33
      -2

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

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

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

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

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

      https://github.com/kelbon/AnyAny

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


      1. feelamee
        15.01.2024 16:33

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

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

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

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


        1. 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 на расте написать невозможно. Там впринципе условие "или" в дженериках нереализуемо


          1. feelamee
            15.01.2024 16:33
            +2

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

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

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

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

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

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


            1. 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;


      1. pooqpooq
        15.01.2024 16:33
        +2

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

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


  1. dalerank
    15.01.2024 16:33
    +4

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


    1. rukhi7
      15.01.2024 16:33
      -3

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

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

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

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

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

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

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

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

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


    1. ZirakZigil
      15.01.2024 16:33

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

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


  1. mentin
    15.01.2024 16:33
    +3

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


  1. 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;


    1. AnthonyMikh
      15.01.2024 16:33
      +3

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


  1. LAutour
    15.01.2024 16:33

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


  1. 0Bannon
    15.01.2024 16:33
    +5

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


    1. ParaMara
      15.01.2024 16:33

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


    1. voldemar_d
      15.01.2024 16:33
      +2

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


  1. Melirius
    15.01.2024 16:33
    +1

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


    1. pooqpooq
      15.01.2024 16:33
      +1

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


  1. cortl
    15.01.2024 16:33
    -3

    • 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++.


    1. pooqpooq
      15.01.2024 16:33
      +7

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

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

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

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


      1. NN1
        15.01.2024 16:33

        1. cortl
          15.01.2024 16:33

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

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


          1. 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. Вне зависимости от того, как вы собираетесь использовать полученное значение.


            1. cortl
              15.01.2024 16:33

              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 должен работать нормально.


              1. 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 здесь.


                1. 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. А может он хочет узнать, что было на стеке в данном месте за мгновение до вызова и он это с некоторой вероятностью узнает.


                  1. pooqpooq
                    15.01.2024 16:33
                    +1

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

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


                    1. cortl
                      15.01.2024 16:33

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


                      1. ZirakZigil
                        15.01.2024 16:33
                        +1

                        В свежем стандарте будет в §6.7.4/2.


                      1. 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 про мусор.


                      1. ZirakZigil
                        15.01.2024 16:33

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

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

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


                  1. KanuTaH
                    15.01.2024 16:33
                    +1

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


                    1. cortl
                      15.01.2024 16:33
                      -2

                      std::size_t f(int x)

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

                      if (x) // either x nonzero or UB

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


                1. 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?

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


                  1. KanuTaH
                    15.01.2024 16:33
                    +6

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


      1. cortl
        15.01.2024 16:33

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

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

        union {
        	float f;
        	uint32_t i;
        } value32;
        
        float f = value32.f;
        uint32_t i = value32.i;


        1. 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 требует, чтобы лайфтайм конвертируемого объекта уже действовал, чего здесь не происходит.


          1. cortl
            15.01.2024 16:33
            -2

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

            } value32;

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


            1. ZirakZigil
              15.01.2024 16:33
              +1

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


            1. pooqpooq
              15.01.2024 16:33
              +1

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


              1. cortl
                15.01.2024 16:33

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


                1. 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 закончилось


                  1. eao197
                    15.01.2024 16:33
                    +2

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

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


                    1. ZirakZigil
                      15.01.2024 16:33

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


    1. NN1
      15.01.2024 16:33
      +1

      1. cortl
        15.01.2024 16:33

        Ответ тот же:

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

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


  1. onets
    15.01.2024 16:33

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


  1. sv91
    15.01.2024 16:33
    +1

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


  1. Panzerschrek
    15.01.2024 16:33
    +4

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


    1. cdriper
      15.01.2024 16:33
      +1

  1. Panzerschrek
    15.01.2024 16:33

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

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

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


    1. pooqpooq
      15.01.2024 16:33

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


      1. Panzerschrek
        15.01.2024 16:33

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


  1. 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);
    }


    1. Kelbon
      15.01.2024 16:33

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

      https://godbolt.org/z/nsns843Ws

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

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


      1. sergio_nsk
        15.01.2024 16:33
        -4

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


        1. Kelbon
          15.01.2024 16:33

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

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


          1. gxcreator
            15.01.2024 16:33
            +10

            Вся эта ветка прямо вопиющий срез С++ коммьюнити если честно.


          1. sergio_nsk
            15.01.2024 16:33
            -1

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


    1. Enjection
      15.01.2024 16:33
      +1

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


  1. MtTheDamos
    15.01.2024 16:33
    +2

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

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


    1. Kelbon
      15.01.2024 16:33

      в бусте есть


      1. dalerank
        15.01.2024 16:33
        +3

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


        1. atd
          15.01.2024 16:33

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


    1. TIEugene
      15.01.2024 16:33
      -4

      // use libstdc++-devel
      #include <cstdint>
      #define BigInteger int64_t

      Не?


      1. ZirakZigil
        15.01.2024 16:33
        +1

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


        1. TIEugene
          15.01.2024 16:33
          -1

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

          UB


          1. ZirakZigil
            15.01.2024 16:33

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


      1. MtTheDamos
        15.01.2024 16:33
        +1

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


        1. TIEugene
          15.01.2024 16:33

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

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

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


          1. MtTheDamos
            15.01.2024 16:33

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


            1. TIEugene
              15.01.2024 16:33

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

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

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

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

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

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

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

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


              1. eao197
                15.01.2024 16:33

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

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

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


                1. TIEugene
                  15.01.2024 16:33

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


                  1. eao197
                    15.01.2024 16:33

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

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

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

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


  1. domix32
    15.01.2024 16:33

    early_return

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

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

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

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

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

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


  1. cdriper
    15.01.2024 16:33
    +1

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

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


  1. TIEugene
    15.01.2024 16:33

    Вы же не путаете "C++" и "C++ Standard Library", правда ведь?


    1. atd
      15.01.2024 16:33

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

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


      1. TIEugene
        15.01.2024 16:33

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

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


        1. AskePit Автор
          15.01.2024 16:33

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


          1. TIEugene
            15.01.2024 16:33

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


        1. pooqpooq
          15.01.2024 16:33
          +3

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


          1. TIEugene
            15.01.2024 16:33
            -2

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


            1. pooqpooq
              15.01.2024 16:33
              +3

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

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


              1. 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

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


                1. pooqpooq
                  15.01.2024 16:33
                  +1

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

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

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

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

                  void foo() { typeid(int); }


                  1. TIEugene
                    15.01.2024 16:33
                    -1

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


  1. Tujh
    15.01.2024 16:33

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

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


    1. 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>()
        );
      }


  1. 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 вы меня не особо убедили.


    1. AskePit Автор
      15.01.2024 16:33
      +1

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

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


      1. AnthonyMikh
        15.01.2024 16:33
        +1

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

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


    1. isadora-6th
      15.01.2024 16:33

      static Spell construct(std::string_view name)

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


      1. AnthonyMikh
        15.01.2024 16:33

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


        1. KanuTaH
          15.01.2024 16:33

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

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

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


          1. AnthonyMikh
            15.01.2024 16:33

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


            1. KanuTaH
              15.01.2024 16:33

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

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

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


              1. ZirakZigil
                15.01.2024 16:33
                +1

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

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


                1. KanuTaH
                  15.01.2024 16:33

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


      1. naviUivan
        15.01.2024 16:33

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


  1. buldo
    15.01.2024 16:33
    +3

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

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

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


    1. atd
      15.01.2024 16:33

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

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

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


      1. buldo
        15.01.2024 16:33

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

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

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


  1. 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 и т.п.), чтобы они не проявляли излишний фанатизм и разрешали писать код так, как удобно, а не так, как положено ;-)


  1. Sazonov
    15.01.2024 16:33

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


  1. voldemar_d
    15.01.2024 16:33

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


    1. rukhi7
      15.01.2024 16:33

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

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


      1. voldemar_d
        15.01.2024 16:33

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

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


        1. rukhi7
          15.01.2024 16:33

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

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

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


    1. lorc
      15.01.2024 16:33
      +2

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


  1. TheCalligrapher
    15.01.2024 16:33

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

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

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

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

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