Skip to content

Commit ff21b7d

Browse files
Add string2optional conversion functions
These are intended as helpers for when you want to convert a string to and integer, but don't want to assert the result (e.g. when dealing with user input there's usually something better you can do than outright crashing if the conversion fails), but also don't want to deal with exceptions (which involve more code to write and read and it's easy to handle the wrong set of exceptions, whether it is too many or too few).
1 parent e654aa7 commit ff21b7d

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

src/util/string2int.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ Author: Michael Tautschnig, [email protected]
1111
#include <cerrno>
1212
#include <cstdlib>
1313
#include <limits>
14+
#include <stdexcept>
1415

1516
#include "invariant.h"
17+
#include "narrow.h"
1618

1719
template <typename T>
1820
inline T str2number(const char *str, int base, bool safe)
@@ -89,3 +91,77 @@ unsigned long long int unsafe_string2unsignedlonglong(
8991
{
9092
return str2number<unsigned long long int>(str.c_str(), base, false);
9193
}
94+
95+
template <typename do_conversiont>
96+
auto wrap_string_conversion(do_conversiont do_conversion)
97+
-> optionalt<decltype(do_conversion())>
98+
{
99+
try
100+
{
101+
return do_conversion();
102+
}
103+
catch(const std::invalid_argument &)
104+
{
105+
return nullopt;
106+
}
107+
catch(const std::out_of_range &)
108+
{
109+
return nullopt;
110+
}
111+
}
112+
113+
optionalt<int> string2optional_int(const std::string &str, int base)
114+
{
115+
return wrap_string_conversion(
116+
[&]() { return std::stoi(str, nullptr, base); });
117+
}
118+
119+
optionalt<unsigned> string2optional_unsigned(const std::string &str, int base)
120+
{
121+
return wrap_string_conversion([&]() {
122+
if(str.find('-') != std::string::npos)
123+
{
124+
// stoul wraps around if given negative numbers,
125+
// but since we need to narrow cast this leads to weird behavior
126+
// so we disallow it here
127+
throw std::out_of_range{"negative values not allowed"};
128+
}
129+
auto const ul_value = std::stoul(str, nullptr, base);
130+
auto const u_value = narrow_cast<unsigned>(ul_value);
131+
if(u_value == ul_value)
132+
{
133+
return u_value;
134+
}
135+
else
136+
{
137+
throw std::out_of_range{"conversion out of range"};
138+
}
139+
});
140+
}
141+
142+
optionalt<std::size_t> string2optional_size_t(const std::string &str, int base)
143+
{
144+
static_assert(
145+
sizeof(std::size_t) <= sizeof(unsigned long long),
146+
"unsigned long long is the largest integer type supported by the sto*"
147+
" functions");
148+
return wrap_string_conversion([&]() {
149+
if(str.find('-') != std::string::npos)
150+
{
151+
// stoull wraps around if given negative numbers,
152+
// but since we need to narrow cast this leads to weird behavior
153+
// so we disallow it here
154+
throw std::out_of_range{"negative values not allowed"};
155+
}
156+
auto const ull_value = std::stoull(str, nullptr, base);
157+
auto const size_t_value = narrow_cast<std::size_t>(ull_value);
158+
if(ull_value == size_t_value)
159+
{
160+
return size_t_value;
161+
}
162+
else
163+
{
164+
throw std::out_of_range{"conversion out of range"};
165+
}
166+
});
167+
}

src/util/string2int.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Author: Michael Tautschnig, [email protected]
1010
#ifndef CPROVER_UTIL_STRING2INT_H
1111
#define CPROVER_UTIL_STRING2INT_H
1212

13+
#include "optional.h"
1314
#include <string>
1415

1516
// These check that the string is indeed a valid number,
@@ -30,4 +31,23 @@ long long int unsafe_string2signedlonglong(const std::string &str, int base=10);
3031
long long unsigned int unsafe_string2unsignedlonglong(
3132
const std::string &str, int base=10);
3233

34+
// if we had a `resultt` á la Boost.Outcome (https://ned14.github.io/outcome/)
35+
// we could also return the reason why the conversion failed
36+
37+
/// Convert string to integer as per stoi, but return nullopt when
38+
/// stoi would throw
39+
optionalt<int> string2optional_int(const std::string &, int base = 10);
40+
41+
/// Convert string to unsigned similar to the stoul or stoull functions,
42+
/// return nullopt when the conversion fails.
43+
/// Note: Unlike stoul or stoull negative inputs are disallowed
44+
optionalt<unsigned>
45+
string2optional_unsigned(const std::string &, int base = 10);
46+
47+
/// Convert string to size_t similar to the stoul or stoull functions,
48+
/// return nullopt when the conversion fails.
49+
/// Note: Unlike stoul or stoull negative inputs are disallowed
50+
optionalt<std::size_t>
51+
string2optional_size_t(const std::string &, int base = 10);
52+
3353
#endif // CPROVER_UTIL_STRING2INT_H

unit/util/string2int.cpp

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*******************************************************************\
2+
3+
Module: Unit tests for string2int.h
4+
5+
Author: Diffblue Ltd.
6+
7+
\*******************************************************************/
8+
9+
#include <testing-utils/use_catch.h>
10+
#include <util/string2int.h>
11+
12+
TEST_CASE(
13+
"converting optionally to a valid integer should succeed",
14+
"[core][util][string2int]")
15+
{
16+
REQUIRE(string2optional_int("13") == 13);
17+
REQUIRE(string2optional_int("-5") == -5);
18+
REQUIRE(string2optional_int("c0fefe", 16) == 0xc0fefe);
19+
}
20+
21+
TEST_CASE(
22+
"optionally converting invalid string to integer should return nullopt",
23+
"[core][util][string2int]")
24+
{
25+
REQUIRE(string2optional_int("thirteen") == nullopt);
26+
REQUIRE(string2optional_int("c0fefe") == nullopt);
27+
}
28+
29+
TEST_CASE(
30+
"optionally converting string out of range to integer should return nullopt",
31+
"[core][util][string2int]")
32+
{
33+
REQUIRE(
34+
string2optional_int("0xfffffffffffffffffffffffffffffffffffffffffff", 16) ==
35+
nullopt);
36+
}
37+
38+
TEST_CASE(
39+
"converting optionally to a valid unsigned should succeed",
40+
"[core][util][string2int]")
41+
{
42+
REQUIRE(string2optional_unsigned("13") == 13u);
43+
REQUIRE(string2optional_unsigned("c0fefe", 16) == 0xc0fefeu);
44+
}
45+
46+
TEST_CASE(
47+
"optionally converting invalid string to unsigned should return nullopt",
48+
"[core][util][string2int]")
49+
{
50+
REQUIRE(string2optional_unsigned("thirteen") == nullopt);
51+
REQUIRE(string2optional_unsigned("c0fefe") == nullopt);
52+
}
53+
54+
TEST_CASE(
55+
"optionally converting string out of range to unsigned should return nullopt",
56+
"[core][util][string2int]")
57+
{
58+
REQUIRE(
59+
string2optional_unsigned(
60+
"0xfffffffffffffffffffffffffffffffffffffffffff", 16) == nullopt);
61+
REQUIRE(string2optional_unsigned("-5") == nullopt);
62+
}
63+
64+
TEST_CASE(
65+
"converting optionally to a valid size_t should succeed",
66+
"[core][util][string2int]")
67+
{
68+
REQUIRE(string2optional_size_t("13") == std::size_t{13});
69+
REQUIRE(string2optional_size_t("c0fefe", 16) == std::size_t{0xc0fefe});
70+
}
71+
72+
TEST_CASE(
73+
"optionally converting invalid string to size_t should return nullopt",
74+
"[core][util][string2int]")
75+
{
76+
REQUIRE(string2optional_size_t("thirteen") == nullopt);
77+
REQUIRE(string2optional_size_t("c0fefe") == nullopt);
78+
}
79+
80+
TEST_CASE(
81+
"optionally converting string out of range to size_t should return nullopt",
82+
"[core][util][string2int]")
83+
{
84+
REQUIRE(
85+
string2optional_size_t(
86+
"0xfffffffffffffffffffffffffffffffffffffffffff", 16) == nullopt);
87+
REQUIRE(string2optional_size_t("-5") == nullopt);
88+
}

0 commit comments

Comments
 (0)