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?
source to share
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.
source to share
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.
source to share
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
source to share
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.
source to share
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.
source to share