Enlico
Enlico

Reputation: 28450

How to wrap several boolean flags into struct to pass them to a function with a convenient syntax

In some testing code there's a helper function like this:

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;
};

which essentially builds up something based on some boolean flags.

What concerns me is that the meaning of each bool is hardcoded in the name of the parameters, which is bad because at the call site it's hard to remember which parameter means what (yeah, the IDE can likely eliminate the problem entirely by showing those names when tab completing, but still...):

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

Therefore, I'd like to pass a single object describing the settings. Furthermore, just aggregating them in an object, e.g. std::array<bool,4>.

I would like, instead, to enable a syntax like this:

auto obj = make_smart_condiment(oil + garlic);

which would generate the same obj as the previous call to make_condiment.

This new function would be:

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

Here's my attempt:

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};

However, I have the feeling that I am reinventing the wheel.

Upvotes: 3

Views: 830

Answers (3)

apple apple
apple apple

Reputation: 10604

If you want to use enum as flags, the usual way is merge them with operator | and check them with 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
}

note: the current code (with only the operator +) would not work if you mix | and + like (Salt|Salt)+Salt.


You can also use enum class, just need to define the operators

Upvotes: 1

Joseph Larson
Joseph Larson

Reputation: 9068

I would look at a strong typing library like:

https://github.com/joboccara/NamedType

For a really good video talking about this:

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

When I first saw this, I was a little dismissive, but because the advice came from people I respected, I gave it a chance. The video convinced me.

If you look at CPP Best Practices and dig deeply enough, you'll see the general advice to avoid boolean parameters, especially strings of them. And Jonathan Boccara gives good reasons why your code will be stronger if you don't directly use the raw types, for the very reason that you've already identified.

Upvotes: 0

mattlangford
mattlangford

Reputation: 1260

I think you can remove some of the boilerplate by using a std::bitset. Here is what I came up with:

#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";
}

You don't get the + type syntax, but it's pretty close. It's unfortunate that you have to keep an Option::Max, but not too bad. Also I decided to not use an enum class so that it can be accessed as Ingredients::Salt and implicitly converted to an int. You could explicitly access and cast if you wanted to use enum class.

Upvotes: 1

Related Questions