6

I have an enum class as follows:

enum class Age
{
    Eleven,
    Twelve,
    Thirteen
};

Then I have a method called vector<Person> GetPeopleOfAge(Age age). What would be a good design so that a developer can call this and get the people with 11, 12 and 13? I can call it three times which is pretty bad but I did want to mention that I considered it. I can add an All enumeration and do a check in my method but I don't like the idea of polluting the enum with enumerations like All just to make my case work. I know it's a common method of solving this and some may disagree with me but to me it feels hacky and am looking for an alternative. Maybe I should use something other than an enum?

Pittfall
  • 2,751
  • 6
  • 32
  • 61
  • 1
    Implement GetAllPeople(). Don't use the enum. Seems really easy. – lars Jan 05 '18 at 14:06
  • @lars GetAllPeople will now be an extra method that does the exact same thing as GetPeopleOfAge() so I will now have duplicate code. – Pittfall Jan 05 '18 at 14:07
  • 2
    "..that does the exact same thing as..." I disagree about this. I also don't see how the code of both methods could be identical. – lars Jan 05 '18 at 14:08
  • 1
    @lars I meant it's the same query. I don't think I will keep this question because I don't think I'm explaining myself right. – Pittfall Jan 05 '18 at 14:10
  • 1
    The only way I see that you could keep identical code for both is if you passed a predicate. – François Andrieux Jan 05 '18 at 14:11
  • 2
    Seems like you want the interface represent the way the data access internally works. While the intention of your class is probably to exactly hide that. – lars Jan 05 '18 at 14:22
  • If you want to be able to pass any number of ages, such as `Twelve+Thirteen`, then check this question, and in particular the accepted answer: https://codereview.stackexchange.com/q/183246/151754 – Cris Luengo Jan 05 '18 at 14:22
  • There almost the same question over on Software Engineering: https://softwareengineering.stackexchange.com/questions/357436/is-it-a-good-practice-to-have-a-special-value-all-in-an-enum/ – besc Jan 05 '18 at 14:28
  • Perhaps some hybrid solution utilizing [std::bitset](http://en.cppreference.com/w/cpp/utility/bitset)? [C++ enum flags vs bitset](https://stackoverflow.com/q/45541760/6610379) – Phil Brubaker Jan 05 '18 at 15:26

9 Answers9

6

Whether All is captured explicitly in the enum or implicitly by another mechanism, you have to deal with the abstraction. Given that, I find it better to deal with it explicitly.

You can use the established method of using values of enums such that they can be combined using bitwise OR operators.

enum Age : unsigned int
{
   Eleven   = 000001,
   Twelve   = 000010,
   Thirteen = 000100,
   All      = 000111
};

Then, you can use

// Get all the people of age 11
auto ret1 = GetPeopleOfAge(Age::Eleven);

// Get people of all ages
auto ret2 = GetPeopleOfAge(Age::All);

// Get all the people aged 11 or 13
auto ret3 = GetPeopleOfAge(Age::Eleven | Age::Thirteen);
R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • The `enum` can be a simple one. See the updated answer. – R Sahu Jan 05 '18 at 14:32
  • It looks like every solution I look at is not quite perfect. This one, the enum is not type safe and you are now providing an `All` option in cases that `All` may not be applicable. – Pittfall Jan 05 '18 at 14:37
  • @Pittfall, of course. That's where a larger context is helpful. – R Sahu Jan 05 '18 at 14:42
4

The obvious solution is to dump the enum: Age is a continuous concept, that may be quantized, but never fully enumerated (What is your highest supported age? 120? 130? 200? 1000? Your choice will be either insensibly large, or have the potential to exclude real persons!). Also, when you talk about age, you frequently need to select ranges of ages.

Consequently, age should be either an int or a float. And your GetPeopleOfAge() function should be declared as

vector<Person> GetPeopleOfAge(int minAge, int maxAge);

No need to complicate things with an enum.

cmaster - reinstate monica
  • 38,891
  • 9
  • 62
  • 106
  • 1
    You missed the point which is my fault for providing a fake example but my question is unrelated to people's age, it's just the object I chose. Think about a Gender object, you can not represent it in some sort of range. – Pittfall Jan 05 '18 at 14:40
  • 2
    @Pittfall In that case, I think you need to ask a different question: Every `enum` has its own pecularities, and may call for its own solution. For instance, your `enum` may list flags that can be or'ed together, or it may list a sequence of options allowing for ranges, or may be just a set of unrelated values. There's no one-size-fits-all solution. I have answered for the continuous sequence with range selection case that you asked about. – cmaster - reinstate monica Jan 05 '18 at 14:45
  • yes I should have provided a better object in my example. – Pittfall Jan 05 '18 at 14:47
3

One option is to make the filter parameter optional:

vector<Person> GetPeopleOfAge(std::optional<Age> age = {})

Then, inside the function, use if (age) to check whether age-based filtering should be done or not.

The function should probably be renamed, though, because it does not always give people of a certain age; sometimes, it gives all people.

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
1

You can make GetPeopleOfAge variadic (initializer_list might work as well) and give it a better name:

template <typename... Ts>
vector<Person> GetPeopleOfAnyAge(Ts... ages);
    // Return a vector of all people having any of `ages...`.

Usage:

const auto people = GetPeopleOfAnyAge(
    Age::Eleven, Age::Twelve, Age::Thirteen);

If you commonly get people of all ages, you can create a wrapper:

const auto getAllPeople = []
{ 
    return GetPeopleOfAnyAge(
        Age::Eleven, Age::Twelve, Age::Thirteen); 
};
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • Neat solution. But I feel it's important to point out that this doesn't implicitly get all people, just the ones you specified. Even if you specify all age enum';s values, you are vulnerable in case new ages get added to the enum later. Though using a wrapper mitigates most of the risk. – François Andrieux Jan 05 '18 at 14:21
1

I'd use a predicate to filter out the returned list. Then the caller can use whatever criterion they want for the subset. (Combining ideas from François and Vittorio.)

Example:

#include <algorithm>
#include <initializer_list>
#include <iostream>
#include <ostream>
#include <string>
#include <vector>

using std::cout;
using std::endl;
using std::function;
using std::initializer_list;
using std::ostream;
using std::string;
using std::vector;

enum class Gender { unknown, male, female };

class Person {
  string name;
  int age;
  Gender gender;
public:
  Person(string n, int a, Gender g) : name{move(n)}, age{a}, gender{g} { }
  string Name() const { return name; }
  int Age() const { return age; }
  Gender Gender() const { return gender; }
};

ostream& operator<<(ostream& o, Person const& person) {
  o << person.Name() << "(" << person.Age() << ", ";
  switch (person.Gender()) {
    case Gender::unknown: o << "?"; break;
    case Gender::male: o << "boy"; break;
    case Gender::female: o << "girl"; break;
  }
  o << ")";
  return o;
}

class People {
  vector<Person> people;
public:
  People(initializer_list<Person> l) : people{l} { }
  vector<Person> GetPeople(function<bool(Person const&)> predicate);
};

vector<Person> People::GetPeople(function<bool(Person const&)> predicate) {
  vector<Person> result;
  for (auto const& person : people) {
    if (predicate(person)) {
      result.push_back(person);
    }
  }
  return result;
}

ostream& operator<<(ostream& o, vector<Person> const& vector_of_person) {
  char const* sep = "";
  for (auto const& person : vector_of_person) {
    o << sep << person;
    sep = ", ";
  }
  return o;
}

int main() {
  auto const b = Gender::male;
  auto const g = Gender::female;
  People people = {{"Anu", 13, g}, {"Bob", 11, b}, {"Cat", 12, g}, {"Dex", 11, b}, {"Eli", 12, b}};
  auto ageUnder13 = [](Person const& p) { return p.Age() < 13; };
  cout << people.GetPeople(ageUnder13) << endl;
  auto everyone = [](Person const& p) { return true; };
  cout << people.GetPeople(everyone) << endl;
  auto boys = [](Person const& p) { return p.Gender() == Gender::male; };
  cout << people.GetPeople(boys) << endl;
  return EXIT_SUCCESS;
}
Eljay
  • 4,648
  • 3
  • 16
  • 27
  • This isn't bad, but there is virtual dispatch overhead on a per-person basis, which depending on the number of elements can be significant. Fixing this is a bit annoying; pass in a `gsl::span`-esque type of `const Person`, and use continuation passing style to get the results (to avoid having to allocate just to return multiple values). This kind of optimization should only be done if it turns out you need the performance (and you proved it with profiling!), otherwise this answer is perfect. – Yakk - Adam Nevraumont Jan 05 '18 at 14:56
  • @Yakk • Thanks! Alternatives to the above is using a filtering iterator that acts upon a predicate, or passing in a filtering predicate along with an action function if the predicate passes. Either would get away from building a new vector. Depends on the use case if those would be appropriate. – Eljay Jan 05 '18 at 15:35
1

While R Sahu just has been quicker than me working on the same idea, coming back to your comment:

It looks like every solution I look at is not quite perfect. This one, the enum is not type safe [...]

If you want to retain the scoped enum for whatever reason, you can define the necessary operators yourself, see below (a little bit of work, admitted – well, what about "perfect"?). About the All value: well, no-one said you need to include it, it was R Sahu's preference (while mine is opposite...) – in the end, this is rather a matter of use case, though...

enum class E
{
    E0 = 1 << 0,
    E1 = 1 << 1,
    E2 = 1 << 2,
    E3 = 1 << 3,
};

E operator|(E x, E y)
{
    return static_cast<E>
    (
            static_cast<std::underlying_type<E>::type>(x)
            |
            static_cast<std::underlying_type<E>::type>(y)
    );
}
E operator&(E x, E y)
{
    return static_cast<E>
    (
            static_cast<std::underlying_type<E>::type>(x)
            &
            static_cast<std::underlying_type<E>::type>(y)
    );
}

bool operator==(E x, std::underlying_type<E>::type y)
{
    return static_cast<std::underlying_type<E>::type>(x) == y;
}
bool operator!=(E x, std::underlying_type<E>::type y)
{
    return !(x == y);
}
bool operator==(std::underlying_type<E>::type y, E x)
{
    return x == y;
}
bool operator!=(std::underlying_type<E>::type y, E x)
{
    return x != y;
}

void f(E e)
{
    E person = E::E1;
    if((person & e) != 0)
    {
        // add to list...
    }
}

int main(int argc, char *argv[])
{
    f(E::E0 | E::E1);
    return 0;
}
Aconcagua
  • 24,880
  • 4
  • 34
  • 59
1

Why not be insane?

enum class subset_type {
  include, all
};
struct include_all_t { constexpr include_all_t() {} };
constexpr include_all_t include_all {};

template<class E>
struct subset {
  subset_type type = subset_type::include;
  std::variant<
    std::array<E, 0>, std::array<E, 1>, std::array<E, 2>, std::array<E, 3>, std::array<E, 4>,
    std::vector<E>
  > data = std::array<E,0>{};
  // check if e is in this subset:
  bool operator()( E e ) const {
    // Everything is in the all subset:
    if(type==subset_type::all)
      return true;
    // just do a linear search.  *this is iterable:
    for (E x : *this)
      if (x==e) return true;
    return false;
  }

  // ctor, from one, subset of one:
  subset(E e):
    type(subset_type::include),
    data(std::array<E, 1>{{e}})
  {}
  // ctor from nothing, nothing:
  subset() = default;
  // ctor from {list}, those elements:
  subset(std::initializer_list<E> il):
    type(subset_type::include)
  {
    populate(il);
  }
  // ctor from include_all:
  subset(include_all_t):type(subset_type::all) {}

  // these 3 methods are only valid to call if we are not all:
  E const* begin() const {
    return std::visit( [](auto&& x){ return x.data(); }, data );
  }
  std::size_t size() const {
    return std::visit( [](auto&& x){ return x.size(); }, data );
  }
  E const* end() const {
    return begin()+size();
  }
  // this method is valid if all or not:
  bool empty() const {
    return type!=subset_type::all && size()==0;
  }
  // populate this subset with the contents of srcs, as iterables:
  template<class...Src>
  void populate(Src const&...srcs) {
    std::size_t count = (std::size_t(0) + ... + srcs.size());
    // used to move runtime count to compiletime N:
    auto helper = [&](auto N)->subset& {
      std::array<E, N> v;
      E* ptr = v.data();
      auto add_src = [ptr](auto& src){
        for (E x:src)
          *ptr++ = x;
      };
      (add_src(srcs),...);
      this->data = v;
    };

    // handle fixed size array cases:
    switch(count) {
      case 0: return helper(std::integral_constant<std::size_t, 0>{});
      case 1: return helper(std::integral_constant<std::size_t, 1>{});
      case 2: return helper(std::integral_constant<std::size_t, 2>{});
      case 3: return helper(std::integral_constant<std::size_t, 3>{});
      case 4: return helper(std::integral_constant<std::size_t, 4>{});
      default: break;
    };
    // otherwise use a vector:
    std::vector<E> vec;
    vec.reserve(count);
    auto vec_helper = [&](auto&& c){
      for (E x:c) vec.push_back(c);
    };
    (vec_helper(srcs), ...);
    data = std::move(vec);
  }

  // because what is a set without a union operator?
  friend subset& operator|=( subset& lhs, subset const& rhs ) {
    if (lhs.type==subset_type::all) return lhs;
    if (rhs.type==subset_type::all) {
      lhs.type = subset_type::all
      return lhs;
    }
    populate( lhs, rhs );
    return lhs;
  }
  friend subset operator|( subset lhs, subset const& rhs ) {
    lhs |= rhs;
    return std::move(lhs);
  }
};

C++17 and probably typos.

std::vector<Person> GetPeopleOfAge(subset<Age> age)

you can call it with Age::Eleven, or with include_all, or with {} for none, or with {Age::Eleven, Age::Twelve} for two.

It uses a small buffer optimization to handle up to 4 elements.

If not in all mode, you can iterate over the elements in the subset using range-based for loops.

Adding support for operator&, subset_type::none, subset_type::exclude, and operator~ left as an exercise.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
0

You could do a combination of things to achieve this borrowing bits and pieces from other's comments and answers.

  • Create a variadic template using std::tuple
  • Use Bit logic on your enum(s) - typical what is seen in many API functions to use multiple settings such as: glClearColor( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); These are typically called piping using bitfields.
  • Create an instance of an object that is a result of a single vector of each where that vector is accumulated so to speak using the above mechanisms.

Something of this nature may work for you:

enum class Gender {
    male,
    female,
    both,
    other
};

class age_constraints {
public:
    const static unsigned min_age { 1 };
    const static unsigned max_age { 130 };
protected:
    age_constraints() = default;
}; typedef age_constraints age_range;

class attributes {
public:
    std::string firstName;
    std::string middleNameOrInitial;
    std::string lastName;
    unsigned age;
    Gender gender;

    attributes() = default;
    attributes( const std::string& first, const std::string& middle, const std::string& last, 
                const unsigned& ageIn, const Gender& gend ) :
        firstName( first ),
        middleNameOrInitial( middle ),
        lastName( last ),
        age( ageIn ),
        gender( gend ) 
    {}
};

class Person {
private:
    attributes attribs;

public:
    Person() = default;
    explicit Person( const attributes& attribsIn ) : attribs( attribsIn ) {}
    Person( const std::string& firstName, const std::string& middle, const std::string& lastName, 
            const unsigned& age, const Gender& gender ) :
        attribs( firstName, middle, lastName, age, gender ) {}

    // other methods
};

class Database {
public:
    const static age_range range;
private:
    std::vector<std::shared_ptr<Person>> peopleOnFile;

public:
    Database() = default;

    void addPerson( const Person&& person ) {
        peopleOnFile.emplace_back( new Person( person ) );
    }

    template<bool all = false>
    std::vector<Person> getPeopleByAges( unsigned minAge, unsigned maxAge, unsigned ages... ) {
        std::tuple<ages> agesToGet( ages... );

        std::vector<Person> peopleToGet;

        // compare tuple ages with the ages in Person::attributes - don't forget that you can use the age_constraints for assistance

        // populate peopleToGet with appropriate age range
            return peopleToGet;
    }

    template<bool all = true>
    std::vector<Person> getPeopleByAges() {
        return peopleOnFile;
    }
};

In the database example above: what I've done in pseudo code is shown that the heavy or bulk work of the code is done in the version of the function that searches within a range, where the overloaded method to find all doesn't take any parameters and just returns the full vector.

Francis Cugler
  • 7,788
  • 2
  • 28
  • 59
0

I have taken some ideas from all of the answers I've seen and here's the best solution I could come up with.

class People
{
   public:
   GetPeopleOfAllAges()
   {
       GetPeopleOfAge();
   }

   private:
   GetPeopleOfAge(Age age = NULL)
   {
       if (age == NULL ||
          *age == Person.age)
       {
           // Add Person to list.
       }
   }
}

In this scenario, NULL means get me everything which is not ideal but at least it is hidden away from the UI layer and I am not violating the age property on Person which cannot be All Would love to hear some thoughts on the approach.

Pittfall
  • 2,751
  • 6
  • 32
  • 61