如何在C++枚举中添加“全部”选项的良好设计?

6

我有一个如下的枚举类:

enum class Age
{
    Eleven,
    Twelve,
    Thirteen
};

那么我有一个叫做 vector<Person> GetPeopleOfAge(Age age) 的方法。如何设计才能让开发者调用它并获取年龄为11、12和13岁的人?虽然我考虑过调用三次,但这样做太糟糕了。我可以添加一个All枚举并在我的方法中进行检查,但我不喜欢用All这样的枚举来污染枚举。我知道这是解决问题的常见方法,有些人可能不同意我的看法,但对我来说,这种方法感觉很差劲,我正在寻找一种替代方案。也许我应该使用其他东西而不是枚举?


1
实现GetAllPeople()函数。不要使用枚举。看起来非常简单。 - lars
2
我不同意这个。我也看不出两种方法的代码如何完全相同。 - lars
1
@lars 我的意思是这是同一个查询。我不认为我会保留这个问题,因为我觉得我没有表达清楚。 - Pittfall
1
我所能想到的唯一方法是,如果你传递一个谓词,那么你可以保持两者完全相同的代码。 - François Andrieux
2
似乎你想让接口代表数据访问内部的工作方式。而你的类的意图可能正是要完全隐藏它。 - lars
显示剩余4条评论
9个回答

6
无论是在枚举中明确地捕获All,还是通过另一种机制隐式地捕获它,您都必须处理抽象概念。考虑到这一点,我认为更好的方法是明确处理它。
您可以使用已建立的枚举值的方法,使它们可以使用按位OR运算符进行组合。
enum Age : unsigned int
{
   Eleven   = 000001,
   Twelve   = 000010,
   Thirteen = 000100,
   All      = 000111
};

然后,您可以使用


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

“enum”可以很简单。请查看更新后的答案。 - R Sahu
看起来我看到的每个解决方案都不是完美的。这个枚举不是类型安全的,而且在某些情况下提供了“全部”选项,可能并不适用。 - Pittfall
@Pittfall,当然。这就是更大的上下文有帮助的地方。 - R Sahu

4
明显的解决方案是放弃枚举: 年龄是一个连续的概念,可以量化,但永远不会被完全枚举(你支持的最高年龄是多少?120?130?200?1000?你选择的要么非常大,要么有可能排除真实存在的人!)。而且,当你谈论年龄时,经常需要选择年龄的范围。因此,年龄应该是一个int或float类型。并且您的GetPeopleOfAge()函数应该声明为:
vector<Person> GetPeopleOfAge(int minAge, int maxAge);

不需要通过 enum 使事情变得复杂。


1
你没有理解重点,这是我提供了一个虚假的例子导致的我的错,但我的问题与人们的年龄无关,只是我选择的对象。想象一下性别对象,你不能用某种范围来表示它。 - Pittfall
2
@Pittfall 在这种情况下,我认为你需要问一个不同的问题:每个“枚举”都有其自己的特点,并可能需要其自己的解决方案。例如,您的“枚举”可能列出可以或在一起的标志,也可能列出允许范围的选项序列,或者可能只是一组无关的值。没有一种通用的解决方案。我已经回答了你所问的连续序列和范围选择的情况。 - cmaster - reinstate monica
是的,我应该在我的示例中提供一个更好的对象。 - Pittfall

3

一种选择是将过滤器参数设为可选:

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

然后,在函数内部使用 if (age) 来检查是否应该进行基于年龄的过滤。

不过,这个函数可能需要重新命名,因为它并不总是只给出某个特定年龄段的人;有时,它会给出所有人的信息。


这是一个不错的解决方案,但现在一切都不等于全部,可能会令人困惑。 - Pittfall

1
你可以将 GetPeopleOfAge 设为可变参数函数 (initializer_list 也可以使用) 并给它一个更好的名称:
template <typename... Ts>
vector<Person> GetPeopleOfAnyAge(Ts... ages);
    // Return a vector of all people having any of `ages...`.

使用方法:

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

如果您经常涉及各个年龄段的人群,您可以创建一个包装器:
const auto getAllPeople = []
{ 
    return GetPeopleOfAnyAge(
        Age::Eleven, Age::Twelve, Age::Thirteen); 
};

整洁的解决方案。但我认为重要的是指出,这并不隐含地获取所有人,只获取您指定的人。即使您指定了所有年龄枚举值,如果将来枚举中添加了新的年龄,您仍然会受到影响。尽管使用包装器可以减轻大部分风险。 - François Andrieux

1
我会使用谓词来过滤返回的列表。然后调用者可以使用任何标准来选择子集。(结合了François和Vittorio的想法。)
示例:
#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;
}

这并不糟糕,但每个人都有虚拟调度开销,这取决于元素的数量,可能会很大。修复这个问题有点麻烦;传递一个类似于gsl::spanconst Person类型,并使用续传样式获取结果(以避免为了返回多个值而分配内存)。只有在证明需要性能时(通过分析!)才应进行此类优化,否则此答案是完美的。 - Yakk - Adam Nevraumont
@Yakk • 谢谢!除此之外,还有一种替代方法是使用一个基于谓词的过滤迭代器或者在谓词通过时同时传入一个过滤谓词和操作函数。这两种方法都能避免构建新的向量。根据具体情况来看,选择哪种方式更适合。 - Eljay

1
虽然R Sahu在同一想法上的工作比我快,但回到您的评论:
看起来每个解决方案都不是完美的。这个枚举不是类型安全的[...]。
如果您想保留作用域枚举,出于任何原因,您可以自己定义必要的运算符,请参见下面的内容(需要一些工作,承认 - 好吧,“完美”怎么样?)。关于All值:好吧,没有人说您需要包含它,这是R Sahu的偏好(而我的相反...)- 最终,这更多是用例问题,尽管...
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;
}

1
为什么不疯狂呢?
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,可能有打字错误。

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

你可以使用Age::Eleveninclude_all{}(表示没有),或者使用{Age::Eleven, Age::Twelve}(表示两个)来调用它。
它使用小缓冲区优化来处理最多4个元素。
如果不在all模式下,您可以使用基于范围的循环迭代子集中的元素。
支持添加operator&subset_type::nonesubset_type::excludeoperator~留作练习。

0
您可以结合他人的评论和答案来实现此目标,从中借鉴一些要素和技巧。
以下是实现此目标的方法:
  • 使用 std::tuple 创建可变参数模板。
  • 对您的 enum(s) 使用比特逻辑——这是许多 API 函数中常见的做法,以使用多个设置,例如:glClearColor( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); 这些通常称为管道化,使用位域。
  • 创建一个对象实例,该对象是每个向量的单一累加结果,使用上述机制进行操作。
以下内容可能对您有所帮助:
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;
    }
};

在上面的数据库示例中:我在伪代码中展示了代码的繁重或大量工作是在搜索范围内的函数版本中完成的,其中重载的查找所有方法不需要任何参数,只返回完整的向量。

0

我从所有答案中获得了一些想法,这是我能想到的最佳解决方案。

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

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

在这种情况下,NULL 表示获取我所有的东西,这并不理想,但至少它被隐藏在 UI 层之外,我没有违反 Person 的 age 属性,该属性不能是 All。很想听听对这种方法的看法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接