Skip to content

Commit db48d39

Browse files
author
pananton
committed
feat userver: add non-throwing version of 'utils::FromString' to be used in performance-critical places
Relates: <https://nda.ya.ru/t/Dx1JoAAr7M6sew> commit_hash:35d6a489bf27ea029ee691651b82802ed8c3d8db
1 parent 6f3f945 commit db48d39

File tree

3 files changed

+121
-48
lines changed

3 files changed

+121
-48
lines changed

universal/include/userver/utils/from_string.hpp

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,60 @@
99
#include <charconv>
1010
#include <cstdint>
1111
#include <cstdlib>
12-
#include <limits>
1312
#include <string>
1413
#include <string_view>
1514
#include <type_traits>
16-
#include <typeindex>
1715
#include <typeinfo>
1816

17+
#include <userver/utils/expected.hpp>
1918
#include <userver/utils/zstring_view.hpp>
2019

2120
USERVER_NAMESPACE_BEGIN
2221

2322
namespace utils {
2423

24+
/// @brief Conversion error code.
25+
enum class FromStringErrorCode {
26+
/// @brief String contains leading whitespace characters.
27+
kLeadingSpaces = 1,
28+
29+
/// @brief String contains invalid (non-digit) characters at the end.
30+
kTrailingJunk = 2,
31+
32+
/// @brief String does not contain a number.
33+
kNoNumber = 3,
34+
35+
/// @brief Conversion result is out of the valid range of the specified type.
36+
kOverflow = 4
37+
};
38+
39+
/// @brief Converts @a code to string representation.
40+
constexpr inline std::string_view ToString(FromStringErrorCode code) noexcept {
41+
switch (code) {
42+
case FromStringErrorCode::kLeadingSpaces:
43+
return "leading spaces are not allowed";
44+
case FromStringErrorCode::kTrailingJunk:
45+
return "extra junk at the end of the string is not allowed";
46+
case FromStringErrorCode::kNoNumber:
47+
return "no number found";
48+
case FromStringErrorCode::kOverflow:
49+
return "overflow";
50+
default:
51+
return "unknown";
52+
}
53+
}
54+
55+
/// @brief Function `utils::FromString` exception type.
2556
class FromStringException : public std::runtime_error {
2657
public:
27-
using std::runtime_error::runtime_error;
58+
/// @brief Creates exception for @a code .
59+
FromStringException(FromStringErrorCode code, const std::string& what);
60+
61+
/// @brief Returns conversion error code.
62+
FromStringErrorCode GetCode() const noexcept { return code_; }
63+
64+
private:
65+
FromStringErrorCode code_;
2866
};
2967

3068
namespace impl {
@@ -48,24 +86,25 @@ template <class T>
4886
inline constexpr bool kIsFromCharsConvertible = IsFromCharsConvertible<T>::value;
4987

5088
[[noreturn]] void ThrowFromStringException(
51-
std::string_view message,
89+
FromStringErrorCode code,
5290
std::string_view input,
53-
std::type_index result_type
91+
const std::type_info& result_type
5492
);
5593

5694
template <typename T>
57-
std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, T> FromString(utils::zstring_view str) {
95+
std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, expected<T, FromStringErrorCode>>
96+
FromString(utils::zstring_view str) noexcept {
5897
static_assert(!std::is_const_v<T> && !std::is_volatile_v<T>);
5998
static_assert(!std::is_reference_v<T>);
6099

61100
if (str.empty()) {
62-
impl::ThrowFromStringException("empty string", str, typeid(T));
101+
return unexpected{FromStringErrorCode::kNoNumber};
63102
}
64103
if (std::isspace(str.front())) {
65-
impl::ThrowFromStringException("leading spaces are not allowed", str, typeid(T));
104+
return unexpected{FromStringErrorCode::kLeadingSpaces};
66105
}
67106
if (str.size() > 2 && str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) {
68-
impl::ThrowFromStringException("extra junk at the end of the string is not allowed", str, typeid(T));
107+
return unexpected{FromStringErrorCode::kTrailingJunk};
69108
}
70109

71110
errno = 0;
@@ -82,36 +121,35 @@ std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, T>
82121
}();
83122

84123
if (errno == ERANGE && !(result < 1 && result > 0.0)) {
85-
impl::ThrowFromStringException("overflow", str, typeid(T));
124+
return unexpected{FromStringErrorCode::kOverflow};
86125
}
87126

88127
if (end == str.c_str()) {
89-
impl::ThrowFromStringException("no number found", str, typeid(T));
128+
return unexpected{FromStringErrorCode::kNoNumber};
90129
}
91130

92131
if (end != str.data() + str.size()) {
93-
if (std::isspace(*end)) {
94-
impl::ThrowFromStringException("trailing spaces are not allowed", str, typeid(T));
95-
} else {
96-
impl::ThrowFromStringException("extra junk at the end of the string is not allowed", str, typeid(T));
97-
}
132+
return unexpected{FromStringErrorCode::kTrailingJunk};
98133
}
99134

100135
return result;
101136
}
102137

103138
template <typename T>
104-
std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, T> FromString(const std::string& str) {
139+
std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, expected<T, FromStringErrorCode>>
140+
FromString(const std::string& str) noexcept {
105141
return impl::FromString<T>(utils::zstring_view{str});
106142
}
107143

108144
template <typename T>
109-
std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, T> FromString(const char* str) {
145+
std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, expected<T, FromStringErrorCode>>
146+
FromString(const char* str) noexcept {
110147
return impl::FromString<T>(utils::zstring_view{str});
111148
}
112149

113150
template <typename T>
114-
std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, T> FromString(std::string_view str) {
151+
std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, expected<T, FromStringErrorCode>>
152+
FromString(std::string_view str) noexcept {
115153
static constexpr std::size_t kSmallBufferSize = 32;
116154

117155
if (str.size() >= kSmallBufferSize) {
@@ -127,22 +165,23 @@ std::enable_if_t<std::is_floating_point_v<T> && !kIsFromCharsConvertible<T>, T>
127165
}
128166

129167
template <typename T>
130-
std::enable_if_t<kIsFromCharsConvertible<T>, T> FromString(std::string_view str) {
168+
std::enable_if_t<kIsFromCharsConvertible<T>, expected<T, FromStringErrorCode>> FromString(std::string_view str
169+
) noexcept {
131170
static_assert(!std::is_const_v<T> && !std::is_volatile_v<T>);
132171
static_assert(!std::is_reference_v<T>);
133172

134173
if (str.empty()) {
135-
impl::ThrowFromStringException("empty string", str, typeid(T));
174+
return unexpected{FromStringErrorCode::kNoNumber};
136175
}
137176
if (std::isspace(str[0])) {
138-
impl::ThrowFromStringException("leading spaces are not allowed", str, typeid(T));
177+
return unexpected{FromStringErrorCode::kLeadingSpaces};
139178
}
140179

141180
std::size_t offset = 0;
142181

143182
// to allow leading plus
144183
if (str.size() > 1 && str[0] == '+' && str[1] == '-') {
145-
impl::ThrowFromStringException("no number found", str, typeid(T));
184+
return unexpected{FromStringErrorCode::kNoNumber};
146185
}
147186
if (str[0] == '+') {
148187
offset = 1;
@@ -157,22 +196,18 @@ std::enable_if_t<kIsFromCharsConvertible<T>, T> FromString(std::string_view str)
157196
const auto [end, error_code] = std::from_chars(str.data() + offset, str.data() + str.size(), result);
158197

159198
if (error_code == std::errc::result_out_of_range) {
160-
impl::ThrowFromStringException("overflow", str, typeid(T));
199+
return unexpected{FromStringErrorCode::kOverflow};
161200
}
162201
if (error_code == std::errc::invalid_argument) {
163-
impl::ThrowFromStringException("no number found", str, typeid(T));
202+
return unexpected{FromStringErrorCode::kNoNumber};
164203
}
165204

166205
if (std::is_unsigned_v<T> && str[0] == '-' && result != 0) {
167-
impl::ThrowFromStringException("overflow", str, typeid(T));
206+
return unexpected{FromStringErrorCode::kOverflow};
168207
}
169208

170209
if (end != str.data() + str.size()) {
171-
if (std::isspace(*end)) {
172-
impl::ThrowFromStringException("trailing spaces are not allowed", str, typeid(T));
173-
} else {
174-
impl::ThrowFromStringException("extra junk at the end of the string is not allowed", str, typeid(T));
175-
}
210+
return unexpected{FromStringErrorCode::kTrailingJunk};
176211
}
177212

178213
return result;
@@ -199,6 +234,31 @@ template <
199234
typename StringType,
200235
typename = std::enable_if_t<std::is_convertible_v<StringType, std::string_view>>>
201236
T FromString(const StringType& str) {
237+
const auto result = impl::FromString<T>(str);
238+
239+
if (result) {
240+
return result.value();
241+
} else {
242+
impl::ThrowFromStringException(result.error(), str, typeid(T));
243+
}
244+
}
245+
246+
/// @brief Extract the number contained in the string. No space characters or
247+
/// other extra characters allowed. Supported types:
248+
///
249+
/// - Integer types. Leading plus or minus is allowed. The number is always
250+
/// base-10.
251+
/// - Floating-point types. The accepted number format is identical to
252+
/// `std::strtod`.
253+
///
254+
/// @tparam T The type of the number to be parsed
255+
/// @param str The string that contains the number
256+
/// @return `utils::expected` with the conversion result or error code
257+
template <
258+
typename T,
259+
typename StringType,
260+
typename = std::enable_if_t<std::is_convertible_v<StringType, std::string_view>>>
261+
expected<T, FromStringErrorCode> FromStringNoThrow(const StringType& str) noexcept {
202262
return impl::FromString<T>(str);
203263
}
204264

universal/src/utils/from_string.cpp

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,25 @@ namespace utils {
1212

1313
namespace impl {
1414

15-
[[noreturn]] void ThrowFromStringException(
16-
std::string_view message,
17-
std::string_view input,
18-
std::type_index result_type
19-
) {
20-
throw FromStringException(fmt::format(
21-
R"(utils::FromString error: "{}" while converting "{}" to {})",
22-
message,
23-
input,
24-
compiler::GetTypeName(result_type)
25-
));
15+
void ThrowFromStringException(FromStringErrorCode code, std::string_view input, const std::type_info& result_type) {
16+
throw FromStringException(
17+
code,
18+
fmt::format(
19+
R"(utils::FromString error: "{}" while converting "{}" to {})",
20+
ToString(code),
21+
input,
22+
compiler::GetTypeName(result_type)
23+
)
24+
);
2625
}
2726

2827
} // namespace impl
2928

29+
FromStringException::FromStringException(FromStringErrorCode code, const std::string& what)
30+
: std::runtime_error(what),
31+
code_(code)
32+
{}
33+
3034
std::int64_t FromHexString(std::string_view str) {
3135
std::int64_t result{};
3236
const auto* str_begin = str.data();

universal/src/utils/from_string_test.cpp

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,28 @@ std::string ToString(T value) {
3030
}
3131
}
3232

33+
template <typename T>
34+
std::string GetDiagnosticString(std::string_view input) {
35+
return fmt::format("type = {}, input = {}", compiler::GetTypeName<T>(), input);
36+
}
37+
3338
template <typename T>
3439
auto TestInvalid(const std::string& input) {
35-
ASSERT_THROW(utils::FromString<T>(input), utils::FromStringException)
36-
<< "type = " << compiler::GetTypeName<T>() << ", input = \"" << input << "\"";
40+
ASSERT_FALSE(utils::FromStringNoThrow<T>(input)) << GetDiagnosticString<T>(input);
41+
ASSERT_THROW(utils::FromString<T>(input), utils::FromStringException) << GetDiagnosticString<T>(input);
3742
}
3843

3944
template <typename StringType, typename T>
4045
auto CheckConverts(StringType input, T expected_result) {
4146
T actual_result{};
42-
ASSERT_NO_THROW(actual_result = utils::FromString<T>(input))
43-
<< "type = " << compiler::GetTypeName<T>() << ", input = \"" << input << "\"";
44-
ASSERT_EQ(actual_result, expected_result)
45-
<< "type = " << compiler::GetTypeName<T>() << ", input = \"" << input << "\"";
47+
48+
ASSERT_NO_THROW(actual_result = utils::FromStringNoThrow<T>(input).value()) << GetDiagnosticString<T>(input);
49+
ASSERT_EQ(actual_result, expected_result) << GetDiagnosticString<T>(input);
50+
51+
actual_result = T{};
52+
53+
ASSERT_NO_THROW(actual_result = utils::FromString<T>(input)) << GetDiagnosticString<T>(input);
54+
ASSERT_EQ(actual_result, expected_result) << GetDiagnosticString<T>(input);
4655
}
4756

4857
template <typename T>

0 commit comments

Comments
 (0)