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

В некотором коде тестирования есть вспомогательная функция, подобная этой:

auto make_condiment(bool salt, bool pepper, bool oil, bool garlic) {
    // assumes that first bool is salt, second is pepper,
    // and so on...
    //
    // Make up something according to flags
    return something;
};

который по существу создает something на основе некоторых флагов boolean.

Что меня беспокоит, так это то, что значение каждого bool жестко закодировано в именах параметров, что плохо, потому что на месте вызова трудно вспомнить, какой параметр что означает (да, IDE, вероятно, может полностью устранить проблему, показывая эти имена при завершении табуляции, но все же...):

// at the call site:
auto obj = make_condiment(false, false, true, true); // what ingredients am I using and what not?

Поэтому я хотел бы передать один объект, описывающий настройки. Кроме того, просто объединяя их в объект, например. std::array<bool,4>.

Вместо этого я хотел бы включить такой синтаксис:

auto obj = make_smart_condiment(oil + garlic);

который сгенерирует тот же obj, что и предыдущий вызов make_condiment.

Эта новая функция будет:

auto make_smart_condiment(Ingredients ingredients) {
    // retrieve the individual flags from the input
    bool salt = ingredients.hasSalt();
    bool pepper = ingredients.hasPepper();
    bool oil = ingredients.hasOil();
    bool garlic = ingredients.hasGarlic();
    // same body as make_condiment, or simply:
    return make_condiment(salt, pepper, oil, garlic);
}

Вот моя попытка:

struct Ingredients {
  public:
    enum class INGREDIENTS { Salt = 1, Pepper = 2, Oil = 4, Garlic = 8 };
    explicit Ingredients() : flags{0} {};
    explicit Ingredients(INGREDIENTS const& f) : flags{static_cast<int>(f)} {};
  private:
    explicit Ingredients(int fs) : flags{fs} {}
    int flags; // values 0-15
  public:
    bool hasSalt() const {
        return flags % 2;
    }
    bool hasPepper() const {
        return (flags / 2) % 2;
    }
    bool hasOil() const {
        return (flags / 4) % 2;
    }
    bool hasGarlic() const {
        return (flags / 8) % 2;
    }
    Ingredients operator+(Ingredients const& f) {
        return Ingredients(flags + f.flags);
    }
}
salt{Ingredients::INGREDIENTS::Salt},
pepper{Ingredients::INGREDIENTS::Pepper},
oil{Ingredients::INGREDIENTS::Oil},
garlic{Ingredients::INGREDIENTS::Garlic};

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

  • Есть ли лучший или стандартный способ выполнения вышеизложенного?

  • Может быть, есть шаблон проектирования, который я мог бы/должен использовать?


person Enlico    schedule 08.07.2021    source источник
comment
Ингредиент std::map может быть лучшим решением.   -  person Mansoor    schedule 08.07.2021
comment
Какую версию C++ вы можете использовать? Если вариант C++20, вы можете создать структуру для хранения логических значений, а затем сделать что-то вроде auto obj = make_smart_condiment(Ingrediant{.oil = true, .garlic = true});   -  person NathanOliver    schedule 08.07.2021
comment
return flags & INGREDIENTS::Garlic, возможно, будет более читабельным, чем return (flags / 8) % 2;   -  person Drew Dormann    schedule 08.07.2021
comment
Я определенно предпочел бы, чтобы здесь в качестве оператора использовался |, а не +, так как это намного лучше соответствовало бы классическим аргументам битового флага, даже если семантика технически больше соответствует +.   -  person Frank    schedule 08.07.2021
comment
@Frank, разве это обычно не означает, что операды являются альтернативами?   -  person Enlico    schedule 08.07.2021
comment
@НатанОливер, С++ 17   -  person Enlico    schedule 08.07.2021
comment
Оператор + будет давать неверные результаты, если обе стороны будут иметь одинаковый ингредиент, а | будет работать правильно.   -  person interjay    schedule 08.07.2021
comment
Я полагаю, что все функции hasXxx можно было бы свести к одной функции шаблона?   -  person DS_London    schedule 08.07.2021
comment
разве это обычно не означает, что операды являются альтернативами - Нет, вы обычно используете побитовое ИЛИ для объединения произвольного количества опций вместе.   -  person Ted Lyngmo    schedule 08.07.2021
comment
@TedLyngmo, я явно был глуп.   -  person Enlico    schedule 08.07.2021
comment
@Enlico Нет, боже, я просто хотел прояснить это. Не будь так строг к себе.   -  person Ted Lyngmo    schedule 08.07.2021
comment
@ Фрэнк, почему + в порядке в операции? Разве это не вызывает проблему, которую выделил @interjay?   -  person Enlico    schedule 08.07.2021
comment
Кажется, что для предоставления функции-члена hasXXX() требуется много усилий. По моему опыту, этот тип проблемы обычно решается с битовыми операциями, и тогда вам нужен только enum! my_ingredients | Ing::Pepper комбайны. my_ingredients & Ing::Pepper тесты.   -  person Drew Dormann    schedule 08.07.2021
comment
@DrewDormann, ты хочешь сказать, что я мог бы использовать INGREDIENTS вместо Ingredients? Если это так, я буду рад удалить весь этот код.   -  person Enlico    schedule 08.07.2021
comment
@Enlico, да, может работать что угодно целочисленное. a | b объединяет a и b. (a & b) == b проверяет, есть ли у a все в b. Если b — это всего лишь один ингредиент, вы можете сократить его до a & b.   -  person Drew Dormann    schedule 08.07.2021
comment
@DrewDormann Как бы вы контролировали передачу параметра ингредиентов функциям? Например, кто-то может передать -42 как действительное целое число, но это не будет действительной побитовой комбинацией ингредиентов.   -  person DS_London    schedule 08.07.2021
comment
@DS_London, ваше беспокойство справедливо, но я не думаю, что эти комментарии — подходящее место для групповых вопросов и ответов. Я надеюсь, вы понимаете.   -  person Drew Dormann    schedule 08.07.2021
comment
@DrewDormann Это было несколько риторически: исходная оболочка класса имеет дополнительное преимущество безопасности типов, поскольку класс Ingredients можно заставить работать только с допустимыми экземплярами перечисления INGREDIENTS. Если вы просто используете «сырое» перечисление, комбинация ингредиентов больше не является допустимым экземпляром этого типа перечисления. Таким образом, в этом смысле код ОП имеет полезность помимо предоставления функций «имеет».   -  person DS_London    schedule 08.07.2021


Ответы (3)


Я думаю, вы можете удалить часть шаблона, используя файл std::bitset. Вот что я придумал:

#include <bitset>
#include <cstdint>
#include <iostream>

class Ingredients {
public:
    enum Option : uint8_t {
        Salt = 0,
        Pepper = 1,
        Oil = 2,
        Max = 3
    };

    bool has(Option o) const { return value_[o]; }

    Ingredients(std::initializer_list<Option> opts) {
        for (const Option& opt : opts)
            value_.set(opt);
    }

private:
    std::bitset<Max> value_ {0};
};

int main() {
    Ingredients ingredients{Ingredients::Salt, Ingredients::Pepper};
    
    // prints "10"
    std::cout << ingredients.has(Ingredients::Salt)
              << ingredients.has(Ingredients::Oil) << "\n";
}

Вы не получаете синтаксис типа +, но он довольно близок. Жалко, что приходится держать Option::Max, но не так уж и плохо. Также я решил не использовать enum class, чтобы к нему можно было получить доступ как Ingredients::Salt и неявно преобразовать в int. Вы можете явно получить доступ и выполнить приведение, если хотите использовать enum class.

person mattlangford    schedule 08.07.2021

Если вы хотите использовать enum в качестве флагов, обычным способом является их объединение с помощью operator | и проверка с помощью operator &.

#include <iostream>

enum Ingredients{ Salt = 1, Pepper = 2, Oil = 4, Garlic = 8 };

// If you want to use operator +
Ingredients operator + (Ingredients a,Ingredients b) {
    return Ingredients(a | b);
}

int main()
{
    using std::cout;
    cout << bool( Salt & Ingredients::Salt   ); // has salt
    cout << bool( Salt & Ingredients::Pepper ); // doesn't has pepper

    auto sp = Ingredients::Salt + Ingredients::Pepper;
    cout << bool( sp & Ingredients::Salt     ); // has salt
    cout << bool( sp & Ingredients::Garlic   ); // doesn't has garlic
}

примечание: текущий код (только с operator +) не будет работать, если вы смешаете | и + как (Salt|Salt)+Salt.


Вы также можете использовать enum class, просто нужно определить операторы

person apple apple    schedule 08.07.2021

Я бы посмотрел на сильную библиотеку типов, например:

https://github.com/joboccara/NamedType

Для действительно хорошего видео, говорящего об этом:

https://www.youtube.com/watch?v=fWcnp7Bulc8

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

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

person Joseph Larson    schedule 08.07.2021
comment
Презентация мне понравилась. Я разделяю разочарование Барни Деллара по поводу отсутствия в C++ определений типов без псевдонимов. Мне понравился комментарий аудитории о том, что (скажем) дюймы и метры — это не типы, они являются единицами, а типом является длина; и length * length дает площадь, а area * length дает объем — может быть, поэтому в языке нет определения типов без псевдонимов. - person Eljay; 08.07.2021