How can I templatize a function without repeating code, but only changing the parse function?

I have an existing function that converts a comma-delimited string of numbers to a vector, for example. "1,2,3" becomes [1,2,3]

The function looks something like this:

bool ConvertStringToNumberList(string input, vector<int32_t>& output)
{
  <bunch of code>
  int32_t value = strtol(str, 0, /*base*/ 10);
  <bunch of code>
}

      

I would like to change this to a template function that will work for int32_t, uint32_t, double and float.

The problem is that for each data type, there is a different parsing function (eg strtol, strtoul, strtod, strtof) that can take a different number of parameters (eg strtod () does not take a "base" parameter).

How can I templatize the above code without repeating <bunch of code>

, but only changing the parse function?

+3


source to share


5 answers


Just create a class that will have specializations, each with a static function to parse one of the types you want to support, and use it from the function template.

eg:.



template <typename T>
struct Converter {};

template <>
struct Converter<int32_t> {
   static int32_t Parse(const std::string& str) {
      return strtol(str, 0, /*base*/ 10); 
   }
};

template <>
struct Converter<double> {
   static double Parse(const std::string& str) {
      return atof(str); 
   }
};

template <typename T>
bool ConvertStringToNumberList(string input, vector<T>& output) {
  // <bunch of code>
  T value = Converter<T>::Parse(str);
  // <bunch of code>
}

      

Also, you might have a simple function Parse

that has overloads for each type, but there you might get unexpected promotions.

+3


source


Unless you really want to do all the work yourself, you're probably best off using something other than strtol

to start with. One possibility that would work well here would be boost::lexical_cast

:

template <class T>
ConvertStringToNumberList(string input, vector<T> &output) { 
    <bunch of code>
    T value = boost::lexical_cast<T>(str);
    <bunch more code>
}

      



lexical_cast

is pretty much equivalent to filling a string into a string stream and then reading the specified type, but it provides overloads to optimize for most of the more common types, so in many (most?) typical cases this is pretty fast.

+1


source


You can add custom separators for fetching a stream by setting the value of your mask table to std::ctype_base::space

, use that table to create a facet, create a locale using a facet, flood that locale and use operator>>

or std::istream_iterator

to fetch your stuff.

You will need some headers ...

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

      

An aspect might look like this:

class custom_delimter_facet : public std::ctype<char>
{
public:
  static custom_delimter_facet * new_facet(std::string const &delimeters)
  {
    std::ctype<char>::mask * mask_table 
      = new std::ctype<char>::mask[std::ctype<char>::table_size];
    // Set zero
    std::memset(mask_table, 0, 
            sizeof(std::ctype<char>::mask)*std::ctype<char>::table_size);
    // Set delimeters
    for (auto delim : delimeters)
      mask_table[delim] = std::ctype_base::space;
    // create facet with table
    return new custom_delimter_facet(mask_table);
  }
  custom_delimter_facet(std::ctype<char>::mask const * tbl)
    : std::ctype<char>(tbl, false), m_facet_mask(tbl)
  { }
  ~custom_delimter_facet()
  {
    if (m_facet_mask) delete[] m_facet_mask;
  }
private:
  std::ctype<char>::mask const * m_facet_mask;
};

      

It can be used to create a single, tempalted function that creates a vector of elements with this face:

template<typename T>
std::vector<T> vec_from_string(std::string const &string, 
  std::string const & delimeters = " \n")
{
  // Create input stringstream
  std::istringstream siss(string);
  // Imbue custom delimeted locale
  // facet will be deleted by locale destructor
  siss.imbue(std::locale(siss.getloc(), 
    custom_delimter_facet::new_facet(delimeters)));
  // Fill vector
  std::vector<T> data;
  std::copy(std::istream_iterator<T>(siss),
    std::istream_iterator<T>(),
    std::back_inserter(data));
  // return vector
  return data;
}

      

Small test:

int main()
{ 
  std::string test1("1423::1232:545367:123231;123454;353456:524;24234");
  std::string test2("17,23,55,44,63,57,0");
  std::string test3("13.4472 , 29.2247 , 44.600");

  std::vector<size_t> result1 = vec_from_string<size_t>(test1, ":;\n");
  std::vector<size_t> result2 = vec_from_string<size_t>(test2, " ,\n");
  std::vector<double> result3 = vec_from_string<double>(test3, " ,\n");

  std::cout << "Result 1 size: " << result1.size() << "\n";
  std::cout << "Result 2 size: " << result2.size() << "\n";
  std::cout << "Result 3 size: " << result3.size() << "\n";

  for (auto const & item : result1)
  {
    std::cout << "Res1 item: " << item << "\n";
  }
  for (auto const & item : result2)
  {
    std::cout << "Res2 item: " << item << "\n";
  }
  for (auto const & item : result3)
  {
    std::cout << "Res3 item: " << item << "\n";
  }
}

      

The result I am expecting is the following:

Result 1 size: 8
Result 2 size: 7
Result 3 size: 3
Res1 item: 1423
Res1 item: 1232
Res1 item: 545367
Res1 item: 123231
Res1 item: 123454
Res1 item: 353456
Res1 item: 524
Res1 item: 24234
Res2 item: 17
Res2 item: 23
Res2 item: 55
Res2 item: 44
Res2 item: 63
Res2 item: 57
Res2 item: 0
Res3 item: 13.4472
Res3 item: 29.2247
Res3 item: 44.6
+1


source


In addition to boost:lexical_cast

, you have the option to use std::istringstream

. This will give you the choice of failed conversions to either return an error or throw an exception. In the same way boost:lexical_cast

, the amount of code required to work in your existing code is minimal and doesn't require any additional functionality or overloads to handle the actual conversion.

In the example below, the success of the conversion can be determined by checking the return type operator>>

. By default, string streams return an error instead of throwing an exception, so no additional code is required. This will fit your existing code perfectly.

template<class ValueType>
bool ConvertStringToNumberList(string input, vector<ValueType>& output)
{
    // <bunch of code>

    ValueType value;
    if (!(istringstream(input) >> value))
    {
        //  Conversion faild
        return false;
    }

    // <bunch of code>
    return true;
}

      

The following example lists the exceptions thrown when a conversion fails. In this case, I don't think exceptions will be needed if the data is not considered correct.

template<class ValueType>
bool ConvertStringToNumberList2(string input, vector<ValueType>& output)
{
    // <bunch of code>

    ValueType value;
    istringstream stream(input);
    stream.exceptions(istringstream::failbit);
    stream >> value;

    // <bunch of code>
    return true;
}

      

Both examples pass context input

to a stream of strings to make testing easier for you. Just change input

to any variable containing the text extracted from the input string.

+1


source


In addition to templatizing your function, create a new function that will take care of the line of code

int32_t value = strtol(str, 0, /*base*/ 10);

      

This function doesn't have to be a template - just use an overload and create an appropriate conversion function overload for each of the data types you need. Then call the overloaded function from your template - the function specific to that type will be selected automatically.

0


source







All Articles