开发者

Generic Aggregation of C++ Objects by Attribute When Attribute Name is Unknown at Runtime

I'm currently implementing a system with a number of class's representing objects such as client, business, product etc. Standard business logic. As one might expect each class has a number of standard attributes.

I have a long list of essentially identical requirements such as:

  • the ability to retrieve all business' whose industry is manufacturing.
  • the ability to retrieve all clients based in London

Class business has attribute sector and client has attribute location. Clearly this a relational problem and in pseudo SQL would look something like:

SELECT ALL business in business' WHERE sector == manufacturing

Unfortunately plugging into a DB is not an option.

What I want to do is have a single generic aggregation function whose signature would take the form:

vector<generic> genericAggregation(class, attribute, value);

Where class is the class of object I want to aggregate, attribute and value being the class attribute and value of interest. In my example I've put vector as return type, but this wouldn't work. Probably better to declare a vector of relevant class type and pass it as an argument. But this isn't the main problem.

How can I accept arguments in string form for class, attribute and value and then map these in a generic object aggregation function?

Since it's rude not to post code, below is a dummy program which creates a bunch of objects of imaginatively named classes. Included is a specific aggregation function which returns a vector of B objects whose A object is equal to an id specified at the command line e.g. ..

$ ./aggregations 5

which returns all B's whose A objects 'i' attribute is equal to 5. See below:

#include <iostream>
#include <cstring>
#include <sstream>
#include <vector>

using namespace std;

//First imaginativly names dummy class
class A {
 private:
  int i;
  double d;
  string s; 
 public:
  A(){}
  A(int i, double d, string s) {
   this->i = i;
   this->d = d;
   this->s = s;
  }
  ~A(){}
  int getInt() {return i;} 
  double getDouble() {return d;} 
  string getString() {return s;}
};

//second imaginativly named dummy class
class B {
 private:
  int i;
  double d;
  string s;
  A *a; 
 public:
  B(int i, double d, string s, A *a) {
   this->i = i;
   this->d = d;
   this->s = s;
   this->a = a;
  }
  ~B(){}
  int getInt() {return i;} 
  double getDouble() {return d;} 
  string getString() {return s;}
  A* getA() {return a;}
};

//Containers for dummy class objects
vector<A> a_vec (10);
vector<B> b_vec;//100

//Util function, not important..
string int2string(int number) {
 stringstream ss;
 ss << number;
 return ss.str();
}


//Example function that returns a new vector containing on B objects
//whose A object i attribute is equal to 'id'
vector<B> getBbyA(int id) {
 vector<B> result;
 for(int i = 0; i < b_vec.size(); i++) {
  if(b_vec.at(i).getA()->getInt() == id) {
   result.push_back(b_vec.at(i));
  }
 }
 return result;
}



int main(int argc, char** argv) {

 //Create some A's and B's, each B has an A...
 //Each of the 10 A's are associated with 10 B's.
 for(int i = 0; i < 10; ++i) {
  A a(i, (double)i, int2string(i));
  a_vec.at(i) = a;
  for(int j = 0; j &开发者_StackOverflow中文版lt; 10; j++) {
   B b((i * 10) + j, (double)j, int2string(i), &a_vec.at(i));   
   b_vec.push_back(b);
  }
 }

 //Got some objects so lets do some aggregation

 //Call example aggregation function to return all B objects 
 //whose A object has i attribute equal to argv[1]
 vector<B> result = getBbyA(atoi(argv[1]));

 //If some B's were found print them, else don't...
 if(result.size() != 0) {
  for(int i = 0; i < result.size(); i++) {
   cout << result.at(i).getInt() << " " << result.at(i).getA()->getInt() << endl;
  }
 }
 else {
  cout << "No B's had A's with attribute i equal to " << argv[1] << endl;
 }

 return 0;
}

Compile with:

g++ -o aggregations aggregations.cpp

If you wish :)

Instead of implementing a separate aggregation function (i.e. getBbyA() in the example) I'd like to have a single generic aggregation function which accounts for all possible class attribute pairs such that all aggregation requirements are met.. and in the event additional attributes are added later, or additional aggregation requirements, these will automatically be accounted for.

So there's a few issues here but the main one I'm seeking insight into is how to map a runtime argument to a class attribute.

I hope I've provided enough detail to adequately describe what I'm trying to do...


You will have to store your data in a hashmap or normal map instead of as raw C++ data. It is impossible to go from the string "hai" to A.hai without doing a switch/case/default on every possible value in C++.

As for the previous answer, this is reflection, not RTTI. C++ does have RTTI - that's what typeid() is for. But, C++ definitely doesn't support it, whatever you want to call it.


You've definitely provided enough details :) I hope you understand it's not going to be easy.

What you are looking for is called 'Reflection', the ability of an object to describe itself.

Reflection is difficult, but it can be done nonetheless!

I'm normally a fan of metatemplates and whatnot, but this time I am going to propose a pure OO-Way. It is intrisic in the sense that you will have to actually modify your objects to support this.

The base idea is to do it the way languages that support this idiom natively do it.

1- We adapt the base types

enum Type
{
  Int,
  String
};

class Field
{
public:
  Type type() const { return mType; }

  virtual Field* clone() const = 0;

  virtual std::string toString() const = 0;
  virtual void fromString(const std::string& s) = 0;

  virtual ~Field() {}
protected:
  explicit Field(Type t): mType(t) {}
private:
  Type mType;
};

bool operator==(const Field& lhs, const Field& rhs)
{
  return lhs.type() == rhs.type() && lhs.toString() == rhs.toString();
}

class IntField: public Field
{
public:
  typedef int data_type;

  IntField(): Field(Int), mData() {}

  virtual IntField* clone() const { return new IntField(*this); }

  data_type get() const { return mData; }
  void set(data_type d) { mData = d; }

  virtual std::string toString() const
  { return boost::lexical_cast<std::string>(mData); }

  virtual void fromString(const std::string& s)
  { mData = boost::lexical_cast<data_type>(s); }

private:
  data_type mData;
};

// Type information allow for more efficient information
bool operator==(const IntField& lhs, const IntField& rhs)
{
  return lhs.get() == rhs.get();
}

2- Then we build a class which will be able to hold all these

class Object
{
public:
  virtual ~Object(); // deal with memory

  Field* accessAttribute(const std::string& name);
  const Field* getAttribute(const std::string& name) const;
  void setAttribute(const std::string& name, const Field& value);

protected:
  // Deal with memory
  Object();
  Object(const Object& rhs);
  Object& operator=(const Object& rhs);

  void addAttribute(const std::string& name, const Field& value);

private:
  std::map<std::string, Field*> mFields; // tricky, we must deal with memory here
};

3- Usage:

class Person: public Object
{
public:
  Person(const std::string& surname,
         const std::string& givenname,
         int age): Object()
  {
    this->addAttribute("surname", StringField(surname));
    this->addAttribute("givenname", StringField(givenname));
    this->addAttribute("age", IntField(age));
  }
};

int main(int argc, char* argv[])
{
  // initialize
  std::vector<Person> persons = /**/;

  std::cout << "Please enter an attribute and the expected value" << std::endl;
  std::string name, value;
  std::cin >> name >> value;

  std::vector<Person*> result;
  for (std::vector<Person>::iterator it = persons.begin(), end = persons.end();
       it != end; ++it)
  {
    const Field* field = it->getAttribute(name);
    if (field && field->toString() == value) result.push_back(&(*it));
  }

  std::cout << "Selected persons for " << name
            << " = " << value << " are:\n";
  for (std::vector<Person*>::iterator it = result.begin(), end = result.end();
       it != end; ++it)
  {
    const Person& person = **it;
    std::cout << "  " << person.surname() << " " << person.givenname() << "\n";
  }
  std::cout.flush();
}

There are probably many other ways, more or less automatized. Notably one could think of adaptation structures over existing classes. Something similar to the macro BOOST_FUSION_ADAPT_STRUCT but with the added value of having the name of the given structure.

I am afraid it could prove a bit more obscur though, for no obvious gain... also, I should add that if possible you might be willing to directly code in a language that supports this natively :)


So the answer to the main question, how to map a runtime argument to a class attribute, is You Can't

What you're looking for is usually called Runtime Type Information (RTTI for short), that is the ability to retrieve information about types at runtime.

C++ doesn't support RTTI. For better or for worse, it just doesn't, end of the story.

Therefore, you will have to invent something of your own. In broad strokes, you'll make all your B classes implement an interface that will have a method like getClass( string className ), which will return pointer to your A object, which in turn will also implement another interface that will have a method like getAttribute( string attrName ), and then you'll be able to compare it to your value. Another possibility is for A to have method compareToValue( string attrName, string value ) instead of getAttribute, so that it can deal with the value's mechanics instead of just comaring as strings.

Additionally, you could have corresponding getAllClasses and getAllAttributes methods to retrieve the lists - if you need it.

Also, if you're not bound to C++, there are other platforms which do support RTTI. Java and .NET would be two examples.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜