3

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.

  • Is there any better, or standard, way of accomplishing the above?

  • Is there maybe a design pattern that I could/should use?

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 2
    An ingredient `std::map` may be a better solution. – Mansoor Jul 08 '21 at 14:52
  • 2
    What version of C++ can you use? If C++20 is an option, you could make a struct to hold the booleans, and then you do something like `auto obj = make_smart_condiment(Ingrediant{.oil = true, .garlic = true});` – NathanOliver Jul 08 '21 at 14:53
  • 2
    `return flags & INGREDIENTS::Garlic` would arguably be more readable than `return (flags / 8) % 2;` – Drew Dormann Jul 08 '21 at 14:53
  • 1
    I would definitely rather see `|` used as an operator instead of `+` here, since that would conform a lot better to "classic" bit flag arguments, even if the semantics are technically more aligned with `+`. –  Jul 08 '21 at 15:04
  • @Frank, doesn't that usually implies that the operads are _alternatives_? – Enlico Jul 08 '21 at 15:05
  • @NathanOliver, C++17 – Enlico Jul 08 '21 at 15:06
  • 2
    The `+` operator would give incorrect results if both sides have the same ingredient, while `|` would work correctly. – interjay Jul 08 '21 at 15:07
  • I imagine all the `hasXxx` functions could be reduced to one template function? – DS_London Jul 08 '21 at 15:11
  • "_doesn't that usually implies that the operads are alternatives_" - No, you usually use bitwise _OR_ to combine an arbitrary amount of options together. – Ted Lyngmo Jul 08 '21 at 15:12
  • @TedLyngmo, I was clearly being stupid. – Enlico Jul 08 '21 at 15:13
  • 1
    @Enlico No, jeez, I just wanted to make that clear. Don't be so hard on yourself. – Ted Lyngmo Jul 08 '21 at 15:14
  • @Frank, why is `+` ok in the operation? Doesn't it still cause the problem that @interjay highlighted? – Enlico Jul 08 '21 at 15:15
  • This seems like a lot of engineering to provide a `hasXXX()` member function. In my experience, this type of problem is normally done with bit operations and then you only need the `enum`! `my_ingredients | Ing::Pepper` combines. `my_ingredients & Ing::Pepper` tests. – Drew Dormann Jul 08 '21 at 15:21
  • @DrewDormann, are you saying that I could use `INGREDIENTS` instead of `Ingredients`? If so, I'm happy to delete all that code. – Enlico Jul 08 '21 at 15:23
  • @Enlico yes, anything integer-like can work. `a | b` combines `a` and `b`. `(a & b) == b` tests if `a` has everything in `b`. If `b` is just one ingredient, you can shorten it to just `a & b`. – Drew Dormann Jul 08 '21 at 15:32
  • @DrewDormann How would you police the passing of the ingredients parameter to functions? Eg someone could pass -42 as a valid int, but this would not be a valid bit-wise combination of ingredients. – DS_London Jul 08 '21 at 15:48
  • @DS_London your concern is valid, but I don't believe these comments are the right place for a group Q&A. I hope you understand. – Drew Dormann Jul 08 '21 at 15:56
  • @DrewDormann It was somewhat rhetorical: the original class wrapper has the added benefit of type safety, in that the Ingredients class can be made to only operate with valid instances of the INGREDIENTS enum. If you just use the ‘raw’ enum, a combination of ingredients is no longer a valid instance of that enum type. So in that sense the OP’s code has utility beyond the provision of the ‘has’ functions. – DS_London Jul 08 '21 at 16:04

3 Answers3

1

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.

mattlangford
  • 1,260
  • 1
  • 8
  • 12
1

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

apple apple
  • 10,292
  • 2
  • 16
  • 36
0

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.

Joseph Larson
  • 8,530
  • 1
  • 19
  • 36
  • I liked the presentation. I share Barney Dellar's frustration about C++'s lack of non-aliasing typedefs. I liked the audience comment about (say) inches and meters are *not types*, they are *units*, and the type is **length**; and `length * length` results in **area**, and `area * length` results in **volume** — maybe that's why non-aliasing typedefs are not in the language. – Eljay Jul 08 '21 at 18:10