Pull to refresh
44
0

Типострадалец

Send message

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

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

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

операция сравнения, это адресная арифметика.

Пожалейте сову. Object в Java тоже можно сравнивать на (не)равенство. Это не значит, что на Object есть арифметика.

Так а арифметика указателей тут причём? Указатель может быть невалидным и при этом не быть null

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

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

Оригинальный код
#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 вы меня не особо убедили.

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

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

А как из первого вытекает второе?

А я правильно понимаю, что у сгенерированного cli::options есть конструктор по умолчанию? Если да, то как он работает с параметрами ENUM? И, кстати, что там происходит с ENUM с пустым списком вариантов?

И рамках действительного большого и сложного проекта все эти факторы играют существенно большую роль, чем ужасные недостатки C++ в сравнении с Rust.

То есть все эти факторы будут и в кодовой базе на Rust, и в кодовой базе на C++. Но в C++ будут ещё и грабли с недиагностируемым UB. По моему, выбор очевиден.

Я пытался сделать таймер - передаёшь ему количество миллисекунд и callback, вызываешь в цикле метод с параметром delta, если прошло необходимое количество миллисекунд, он вызывает callback.

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

А зачем ограничения Debug на A и B?

Возможность деплоить только изменённые компоненты у микросервисов нивелируется возможностью использовать слоты деплоя у монолита

Так, а вот можно поподробнее про слоты деплоя? Впервые слышу такой термин.

Истинная утиная типизация есть в шаблонах C++. Там можно в шаблонном коде вызвать у переданного типа метод крякания. И всё будет работать, пока в шаблон передаются типы с нужным методом и сигнатурой.

Угу, вот только forward iterator от input iterator вы так не отличите, потому что набор синтаксических требований одинаков.

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

Или просто использовать num-traits, в которых это уже сделано за тебя. Но без обобщённых литералов не совсем удобно.

В Webassembly нет нативной поддержки сборки мусора, поэтому Blazor вынужден помимо кода приложения компилировать в блоб и весь .NET рантайм. То есть по сути интерпретатор запускает интерпретатор. Так что оно в принципе особо производительным не будет.

а он не может две строчки кода связать (i.e. тривиально развернуть строку)

Напоминаю, что задача "развернуть строку" не нужна на практике от слова совсем и требует довольно нетривиального кода для обработки Unicode хотя на уровне code point, не говоря уже о grapheme cluster.

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

Счётчик ссылок на каждый нетривиальный тип - это не быстро

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

О да, способность решать задачи на Leetcode (с, кстати, крайне кривыми сигнатурами на большинстве языков) — это действительно очень репрезентативная метрика юзабельности языка.


C++ тут, кстати, не лучше — как на нём обратить строку "Привет, мир", например?


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

Information

Rating
Does not participate
Registered
Activity