Hailiang Zhang
Hailiang Zhang

Reputation: 18870

Programming model for classes with variable number and types of const variables

My previous question (Programming model for classes with const variables) received a perfect answer, but now I have a new requirement, and the answer seems not working anymore.

Say I have a class containing several const variables:

class Base
{
    protected:
        const int a, b;
    public:
        Base(string file);
};

The constants need to be initialized in the initialization list, but also need some other method in advance to calculate the values.

The answer was to use a helper class:

class FileParser 
{
public:
  FileParser (const string& file)
  {
    Parse (file);
  }

  int GetA () const { return mA; }
  int GetB () const { return mB; }

private:
  int mA;
  int mB;

  void Parse (const string& file)
  {
    // MAGIC HAPPENS!
    // Parse the file, compute mA and mB, then return
  }
};

This perfectly solved my problem, but now, what about if I have a series of derived class from Base which has different number and types of constants, and I want to use the same helper (FileParser) class? I can't use boost C++, but I have c++11. I tried templates with variadics that return a tuple with variable length, but it seems non-trivial. Following is the modified helper class I tried:

template <typename ... Types>
class LineParser
{
    private:
        std::tuple<Types...> _t; 
    public:
        LineParser(const std::string & line)
        {   
            // local variables
            std::stringstream ss; 

            // parse the line
            ss.str(line);
            for (int i=0; i<sizeof...(Types); i++)
            {   
                ss>>std::get<i>(_t);
            }   
        }   
};

It failed compiling with:

error: the value of ‘i’ is not usable in a constant expression

I can't solve this problem, and I may be looking for some alternate solutions.c++

Upvotes: 1

Views: 83

Answers (1)

John Dibling
John Dibling

Reputation: 101456

So this is getting a little more complex. It's also a bit of an XY Problem, but at least here you're explicit about both X and Y.

Let's start with your proposed approach. This is never going to work:

std::get<i>(_t);

get is a function template, and so i must be an Integral Constant Expression. In other words, i must be known at compile-time.

Since your proposed solution is fundamentally based on a tuple, the whole thing unravels and falls apart when you can't make i and ICE. So, let's forget the proposed approach, and look at the problem again. You have a file with a bunch of stuff in it, presumably separated in to something that looks like fields. Those fields represent (as far as I can tell) different types of data. Suppose this is an example of such a file:

IBM
123.45
1000

Here we have a string, a float and an integer. Different files might have different data entirely, and the data at a given position in one file might not be the same type as the data at the same position in a different file. You then have a bunch of different classes that need to be initialized with these different files, each with it's own collection of different data members of different types, pulled from different positions in the files. Yuck.

Given the complexity of the problem, my natural tendency is to keep the solution as simple as possible. There's enough complexity here already. The simplest possible approach I can think of is to simply have a different concrete LineParser class for each type of file you want to parse. This will however result in code bloat if you have a lot of different types of files, and becomes exponentially more difficult to maintain as that number grows. So let's continue on the assumption you don't want to do that.

One thing that will not increase however is the number of different types of fields in the file. Ultimately, there's really ony a few: strings, integral, floats, and maybe some other special stuff specific to your domain. Even as you add more data files however, the number of types of fields will remain relatively constant. Another constant is the file itself: it's character data. So let's leverage that.

Implement some free functions that convert from the file storage type (character data, I'm assuming here) to the different fields. If you're using Boost, you can use lexical_cast to do most of this. Otherwise you can use stringstream or something else. Here's one possible implementation, there are many others:

template <typename Return> Return As (const std::string& val)
{
  std::stringstream ss;
  ss << val;
  Return retval;
  ss >> retval;
  return retval;
}

Now I'm assuming that for a given Base-type class, you know the positions and types of the fields of interest to you, and those are invariant. For example, for a Base that represents a stock quote, you know the first field is the ticker symbol and it's a string.

Your FileParser class can be generic if all it does is pull everything out of the file and cache it as character data in an array, one element per field in the file. Again, there are many possible implementation here -- my focus is the design, not the actual code.

class LineParser
{
    private:
        std::vector <string> mItems;
    public:
        LineParser(const std::string & fileName)
        {   
          std::ifstream fs(fileName);
          std::copy(
            std::istream_iterator<int>(fs), 
            std::istream_iterator<int>(), 
            std::back_inserter(mItems));
        }   

        std::string GetAt (size_t i) const
        { 
          return mItems [i];
        }
};

Now in the Base constructor, for each const data member, pull a specific item from the LineParser and convert it with your free function:

class Base
{ 
private:
  const std::string mTicker;
  const uint32_t mSize;
  const float mPrice;
public:
  Base (const LineParser& parser)
  : 
    mTicker (As <std::string> (parser.GetAt (0))),  // We know the ticker is at field 0
    mPrice (As <float> (parser.GetAt (1))),  // Price is at field 1...
    mSize (As <uint32_t> (parser.GetAt (2))
  {
  }
};

There's a number of things I like about this approach. For one, even though there are a number of classes and functions involved, each one is simple. Each little gizmo here has a single, clearly defined responsibility, and doesn't try to do too much.

For another, the self-documentation of your business logic code is concise and where it belongs: in the code where the const members are initialized:

 Base (const LineParser& parser)
 : 
   mTicker (As <std::string> (parser.GetAt (0))),  // We know the ticker is at field 0
   mPrice (As <float> (parser.GetAt (1))),  // Price is at field 1...
   mSize (As <uint32_t> (parser.GetAt (2))
 {
 }

The initializer for mTicker for example says "The ticker symbol is a string, and it is pulled from position 1 in the file." Clean.

Upvotes: 2

Related Questions