simplifying this question, taking most of the comment into considerations, I ended up with this version, looking for some feedback
#include <iostream>
#include <tuple>
#include <cassert>
#include <type_traits>
std::tuple<std::string_view, std::string_view> split(std::string_view str, char delimiter)
{
auto pos = str.find(delimiter);
if (pos == std::string_view::npos)
{
return {str, {}};
}
return {str.substr(0, pos), str.substr(pos + 1)};
}
template <typename T>
auto parse(std::string_view str)
{
using T2 = std::unwrap_ref_decay_t<T>;
T2 res;
if constexpr (std::is_same_v<T2, std::string>) { return res = std::string(str);}
if (std::is_floating_point_v<T2>){ return res = std::stod(std::string(str));return res;}
if (std::is_integral_v<T2>) { return res = std::stol(std::string(str));}
throw std::invalid_argument("Invalid type");
}
template <typename T>
constexpr bool is_tuple = false;
template <typename T>
constexpr bool has_content = false;
template <typename T>
constexpr bool is_constructible = false;
template <typename... Ts>
constexpr bool is_tuple<std::tuple<Ts...>> = true;
template <typename... Ts>
constexpr bool has_content<std::tuple<Ts...>> = sizeof...(Ts) > 0;
template <typename... Ts>
constexpr bool is_constructible <std::tuple<Ts...>> = std::conjunction_v<std::is_default_constructible<Ts>...>;
template <typename TupleType>
requires is_tuple<TupleType> &&
has_content<TupleType> &&
is_constructible<TupleType>
auto handle(std::string_view str)
{
TupleType result;
auto parse_one = [&](auto &arg)
{
auto [ field, rest ] = split(str, ',');
arg = parse<decltype(arg)>(field);
str = rest;
};
std::apply([&](auto &...args)
{ (parse_one(args), ...); },
result);
return result;
}
int main()
{
using LineData = std::tuple<std::string, double, int64_t>;
std::string_view input_str = "first_cut,55.3,177";
LineData result = handle<LineData>(input_str);
std::cout << "Parsed values:" << '\n';
std::cout << "String value: " << std::get<0>(result) << '\n';
std::cout << "Double value: " << std::get<1>(result) << '\n';
std::cout << "Int value: " << std::get<2>(result) << '\n';
return 0;
}
1 Answer 1
Simplify the constraints
You defined three type trait checkers: is_tuple
, has_content
and is_constructible
. Note that has_content
is also only ever true if the type passed is a std::tuple
, so is_tuple
is redundant.
The names are also wrong: has_content
doesn't say anything about it checking for tuples, and one would say that a non-empty std::vector
also "has content". Maybe is_nonempty_tuple
is better? One could also argue that while an empty tuple is a corner case, the code you've written will actually handle that correctly, so there is no need to prevent it.
is_constructible
is also misnamed: again despite the name not saying it, it only is ever true for std::tuple
s. But also note that "constructible" is not the same as "default constructible". So it should have been named is_default_constructible_tuple
. However, there is no need to make such a check; std::is_default_constructible_v<TupleType>
already checks that the whole tuple can be default constructed.
While you check that TupleType
is a default-constructible, non-empty tuple, you didn't check whether the types stored in the tuple can actually be parsed. You could write a concept that checks whether you provide a default-constructible, non-empty, parsable std::tuple
, but it would just duplicate the whole body of handle()
inside a requires
expression, which defeats the point.
I'd rather keep it simple:
template <typename TupleType>
requires is_tuple<TupleType> &&
std::is_default_constructible_v<TupleType>
auto handle(std::string_view str) ...
Avoid needing the tuple type to be default-constructible
You can avoid default-constructing a TupleType
by making use of the fact that you can return a value from std::apply()
, and that you can already refer to a variable's name when constructing it:
auto handle(std::string_view str)
{
auto parse_one = [&](auto &arg)
{
auto [ field, rest ] = split(str, ',');
str = rest;
return parse<decltype(arg)>(field);
};
TupleType result = std::apply([&](auto &...args)
{ return TupleType{parse_one(args)...}; },
result);
return result;
}
Simplify returning values from parse()
I think your parse()
is unnecessarily complex because you forgot to add constexpr
to the second and third if
-statement. I think you can completely avoid the temporary variable res
:
template <typename T>
auto parse(std::string_view str)
{
using T2 = std::unwrap_ref_decay_t<T>;
if constexpr (std::is_same_v<T2, std::string>) { return str; }
if constexpr (std::is_floating_point_v<T2>) { return std::stod(std::string(str)); }
if constexpr (std::is_integral_v<T2>) { return std::stol(std::string(str)); }
throw std::invalid_argument("Invalid type");
}
Prefer using std::from_chars()
to parse numeric values
Unfortunately, std::stod()
and std::stoi()
don't take std::string_view
s as arguments, as you have discovered. I recommend you use std::from_chars()
instead. It also doesn't take std::string_view
s, but it takes pointers to the start and end of the string, which you can easily create from a std::string_view
. It also avoids having to deal with integers and floating point values separately:
if constexpr (std::is_arithmetic_v<T2>) {
if (T2 value; std::from_chars(str.begin(), str.end(), value) == std::errc{})
return value;
else
throw std::runtime_error("Parse error");
}
-
\$\begingroup\$ Thank you for your review, will take your input into considerations \$\endgroup\$G. Nass– G. Nass2023年03月21日 21:47:28 +00:00Commented Mar 21, 2023 at 21:47