如何优雅地声明变量集合的子集

7
假设需求如下: 作为一个用户,我希望收集有关一个主题的信息,并当班级收集足够信息时,我希望班级向我返回已收集数据的列表。足够的信息被定义为-当从所有可能信息的子集收集到所有信息时。该子集不固定且提供给班级。
例如,这是所有可能信息的列表:
{
   string name; 
   int age;
   char sex;
   string location;
}

我希望让用户有可能告诉我从哪个数据源(我的类解析数据)获取年龄和性别。

问题在于我不知道如何在没有枚举的情况下传达这一点。基本上,我的枚举解决方案是监听数据源,直到我使用std::includes在2组枚举(已收集、必需)上确定我已经收集到所有数据。

是否有可能不使用枚举来完成这个任务?


我使用枚举来完成这个。 - Lightness Races in Orbit
@LightnessRacesinOrbit 我也是...就像问题中提到的一样 :D 我只是想知道是否有一个更好的方法(商标) - NoSenseEtAl
那是我说我还没有找到的方式。 - Lightness Races in Orbit
你可以像任何脚本语言一样,在底层使用字符串代替枚举,使用哈希表代替结构体。当然,这并不是很高效,也不是非常符合“C ++”的风格,如果这两个方面都很重要的话。另一方面,它允许在运行时定义信息,而不必在编译时就已知。 - hyde
5个回答

3
每当我想将某个逻辑的实现与需要它的位置分离开来,比如这种“收集多少数据才够”的知识,我就会想到一个回调函数。
假设你的类能够收集的所有可能数据都是已知的(例如在你的示例中,nameagesexlocation),那么所有客户端都可以(可能)知道它,而不增加耦合和依赖关系。
我的解决方案是创建一个“评估器”类来封装这个逻辑。客户端创建该类的子类实例,并在请求数据时将其传递给数据收集器;此对象负责决定(并告诉“收集器”),何时收集足够的数据。
#include <string>

// The class that decides when enough data has been collected
// (Provided to class "collector" by its clients)
class evaluator
{
public:
  virtual ~evaluator() {};

  // Notification callbacks; Returning *this aids in chaining
  virtual evaluator& name_collected()     { return *this; }
  virtual evaluator& age_collected()      { return *this; }
  virtual evaluator& sex_collected()      { return *this; }
  virtual evaluator& location_collected() { return *this; }

  // Returns true when sufficient data has been collected
  virtual bool enough() = 0;
};

// The class that collects all the data
class collector
{
public:
  void collect_data( evaluator& e )
  {
    bool enough = false;
    while ( !enough )
    {
      // Listen to data source...

      // When data comes in...
      if ( /* data is name */ )
      {
        name = /* store data */
        enough = e.name_collected().enough();
      }
      else if ( /* data is age */ )
      {
        age = /* store data */
        enough = e.age_collected().enough();
      }
      /* etc. */
    }
  }

  // Data to collect
  std::string name;
  int age;
  char sex;
  std::string location;
};

在您的示例中,您希望特定客户能够指定agesex的组合是足够的。因此,您可以像这样子类化evaluator

class age_and_sex_required : public evaluator
{
public:
  age_and_sex_required()
    : got_age( false )
    , got_sex( false )
  {
  }

  virtual age_and_sex_required& age_collected() override
  {
    got_age = true;
    return *this;
  }

  virtual age_and_sex_required& sex_collected() override
  {
    got_sex = true;
    return *this;
  }

  virtual bool enough() override
  {
    return got_age && got_sex;
  }

private:
  bool got_age;
  bool got_sex;
};

客户端在请求数据时传递此类的实例:
collector c;
c.collect_data( age_and_sex_required() );
collect_data 方法在 age_and_sex_required 实例报告数据收集量“足够”且您没有将任何逻辑、知识、枚举等构建到 collector 类中时退出并返回。此外,“足够”所包含的逻辑是无限可配置的,无需对 collector 类进行进一步更改。
----- 编辑 -----
另一个版本将不使用具有 ..._collected() 方法的类,而仅使用接受 collector 作为参数并返回 boolean 的单个(typedef'd)函数:
#include <functional>
typedef std::function< bool( collector const& ) > evaluator_t;

collector::collect_data(...) 中的代码仅仅会调用

enough = e( *this );

每次收集数据时,这将消除对单独的“evaluator”抽象接口的必要性,但会增加对“collector”类本身的依赖,因为作为“evaluator_t”函数传递的对象将负责检查“collector”对象的状态以评估是否已收集足够的数据(并且需要“collector”具有足够的公共接口来查询其状态)。
bool age_and_sex_required( collector const& c )
{
  // Assuming "age" and "sex" are initialized to -1 and 'X' to indicate "empty"
  // (This could be improved by changing the members of "collector" to use
  // boost::optional<int>, boost::optional<char>, etc.)
  return (c.age >= 0) && (c.sex != 'X');
}

用户必须跳过很多麻烦才能以那种方式调用库函数。当然,这样可以获得完全的控制权(包括意外写出无意义代码的可能性),但这真的有必要吗? - kuroi neko
同意,虽然可能只是一个小绊脚石!一个简单的内联lambda可以很好地工作:c.collect_data( [](collector const& c){ return (c.age >= 0) && (c.sex != 'X'); } );。这肯定比其他一些替代方案更复杂,但它提供了很大的灵活性。这是一个权衡(像总是一样!):什么更重要?简单还是灵活性? - aldo
这个问题由 OP 来回答 :). 我的投票将会给予那些默认工作的解决方案(即,如果你不想使用任何这些花哨的可选字段,只需调用函数),并传递一个简单的参数(可选字段列表),如果你需要一些花哨的事情来处理它们。我不喜欢 C++ 高手做的接口,他们认为用户也是其他 C++ 高手。我的直觉是,掌握 C++ 的人可能只有 1%,但是有很大比例的人认为自己是前面提到的 1% 中的一员(或者假装是为了不失去他们的工作)。 - kuroi neko
例如在这里,您将强制用户在其代码中使用lambda函数,并处理库可以透明地处理的默认值。在我看来,一个清晰的接口应该恰恰相反,即隐藏像默认值这样的不干净的内部细节,并提供一种简单、抽象的方式来指定要处理的数据。 - kuroi neko

2

不确定这是否适用于您,但由于每个项目可能存在或不存在,我想到了boost::optional

{
   boost::optional<string> name; 
   boost::optional<int> age;
   boost::optional<char> sex;
   boost::optional<string> location;
}

你的类可以有一个bool validate()方法,该方法检查所需项目的存在。这可以是一个类方法,也可以作为回调传递。


2
您可以为每个成员定义一个默认值,表示“我是必需的”。
static const string required_name = /* your default name */;
// ...

您还可以使用整数作为位掩码,它的行为类似于一组枚举值。

typedef int mask_type;
static const mask_type name_flag = 0x01;
static const mask_type age_flag = 0x02;
static const mask_type sex_flag = 0x04;
static const mask_type location_flag = 0x08;
//...

mask_type required = name_flag | age_flag; // need to collect name & age
collect(&my_instance, required) // collect and set required values

易于使用,不会带来比单个int更多的开销:

  1. 不再需要值:required &= ~xx_flag
  2. 不再需要任何值:bool(required)
  3. 需要值:bool(required & xx_flag)
  4. ...

这正是我会做的方式。简单、轻量级,没有模板垃圾,而且它能够胜任工作。比强制用户编写几十行代码来使用库要好得多。 - kuroi neko

1

你可以通过使用模板和抽象类来实现这样的行为,像这样做:

class SomeAbstract
{
public:
    virtual bool getRequired()  = 0;
    virtual void setRequired(bool req) = 0;
};

template <class T>
class SomeTemplate
{
    T value;
    bool required;

public:
    TemplateName(T t)
    {
        value = t;
        required = false;
    }
    void setRequired(bool req)
    {
        required = req;
    }
    bool getRequired()
    {
        return required;
    }
    void setValue(T newValue)
    {
        value = newValue;
    }
    T getValue()
    {
        return value;
    }
};

然后,您可以将属性列表声明为相同的类型。

SomeTemplate<string> name; 
SomeTemplate<int> age;
SomeTemplate<char> sex;
SomeTemplate<string> location;

由于模板继承了相同的类型,因此可以将它们存储在 std::vector<SomeAbstract> 中并将它们视为相同的对象。

这不是经过测试的代码,可能还有改进的地方,但我希望你能理解我的意思。


这归结于为每个字段添加一个隐藏的“required”布尔值。您可以通过为每个字段设置默认值来获得相同的结果,这涵盖了99.99%的实际用例(谁会想要在SSE日期中存储MAX_INT或将空字符串作为有效名称?)。 - kuroi neko

1

枚举似乎是最干净的方法来实现这一点,但我想如果你喜欢的话,你可以使用短字符串,并为每种类型的数据使用不同的字符。这样做不太干净,但可能更容易调试。


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