Add function template simgear::strutils::readNonNegativeInt()

This function is similar to simgear::strutils::to_int(), except it is:

  - generic: the return type, selected with the first template
    parameter, can be an arbitrary integral type. This type also defines
    the set of accepted input strings ("values").

  - stricter regarding the input: it must be non-empty and contain only
    valid digits for the specified base (second template parameter). If
    the input doesn't conform to these constraints or is too large to
    fit into the specified type, an exception with a suitable error
    message is raised.

  - faster (12 to 17 times as fast as simgear::strutils::to_int() on my
    system, depending on compilation flags): this is probably a
    consequence of not using std::stringstream to do the conversion.

The function template is only instantiated for <int, 10> and
<unsigned int, 10> in order to be sure not to waste memory (see comments
in strutils.cxx). If you need it for other combinations of type and
base, just enable them by adjusting the corresponding '#if 0 / #endif'
pairs in strutils.cxx and strutils_test.cxx.
This commit is contained in:
Florent Rougon
2017-04-16 20:18:05 +02:00
parent 04ca3ad8f4
commit 1e99c4b2ec
3 changed files with 504 additions and 5 deletions

View File

@@ -22,12 +22,13 @@
#include <simgear_config.h>
#include <ctype.h>
#include <cstring>
#include <string>
#include <sstream>
#include <algorithm>
#include <string.h> // strerror_r() and strerror_s()
#include <errno.h>
#include <type_traits>
#include <cstring> // strerror_r() and strerror_s()
#include <cctype>
#include <cerrno>
#if defined(HAVE_CPP11_CODECVT)
#include <codecvt> // new in C++11
@@ -374,6 +375,173 @@ namespace simgear {
return result;
}
template<>
int digitValue<10>(char c)
{
if ('0' <= c && c <= '9') {
return static_cast<int>(c - '0');
} else {
throw sg_range_exception("invalid as a decimal digit: '" +
std::string(1, c) + "'");
}
}
template<>
int digitValue<16>(char c)
{
if ('0' <= c && c <= '9') {
return static_cast<int>(c - '0');
} else if ('a' <= c && c <= 'f') {
return 10 + static_cast<int>(c - 'a');
} else if ('A' <= c && c <= 'F') {
return 10 + static_cast<int>(c - 'A');
} else {
throw sg_range_exception("invalid as an hexadecimal digit: '" +
std::string(1, c) + "'");
}
}
template<>
std::string numerationBaseAdjective<10>()
{ return std::string("decimal"); }
template<>
std::string numerationBaseAdjective<16>()
{ return std::string("hexadecimal"); }
template<
class T,
int BASE = 10,
typename = typename std::enable_if<std::is_integral<T>::value, T>::type >
T readNonNegativeInt(const std::string& s)
{
static_assert(0 < BASE,
"template value BASE must be a positive integer");
static_assert(BASE <= std::numeric_limits<T>::max(),
"template type T too small: it cannot represent BASE");
T res(0);
T multiplier(1);
T increment;
int digit;
if (s.empty()) {
throw sg_format_exception("expected a non-empty string", s);
}
for (auto it = s.crbegin(); it != s.crend(); it++) {
if (it != s.crbegin()) {
// Check if 'multiplier *= BASE' is going to overflow. This is
// reliable because 'multiplier' and 'BASE' are positive.
if (multiplier > std::numeric_limits<T>::max() / BASE) {
// If all remaining digits are '0', it doesn't matter that
// the multiplier overflows.
if (std::all_of(it, s.crend(),
[](char c){ return (c == '0'); })) {
return res;
} else {
throw sg_range_exception(
"doesn't fit in the specified type: '" + s + "'");
}
}
multiplier *= BASE;
}
try {
digit = digitValue<BASE>(*it);
} catch (const sg_range_exception&) {
throw sg_format_exception(
"expected a string containing " +
numerationBaseAdjective<BASE>() +
" digits only, but got '" + s + "'", s);
}
// Reliable because 'multiplier' is positive
if (digit > 0 &&
multiplier > std::numeric_limits<T>::max() / digit) {
throw sg_range_exception(
"doesn't fit in the specified type: '" + s + "'");
}
increment = multiplier*digit;
if (res > std::numeric_limits<T>::max() - increment) {
throw sg_range_exception(
"doesn't fit in the specified type: '" + s + "'");
}
res += increment;
}
return res;
}
// Explicit template instantiations.
//
// In order to save some bytes for the SimGearCore library[*], we only
// instantiate a small number of variants of readNonNegativeInt() below.
// Just enable the ones you need if they are disabled.
//
// [*] The exact amount depends a lot on what you measure and in which
// circumstances. On Linux amd64 with g++, I measured a cost ranging
// from 2 KB per template in a Release build to 19 KB per template in
// a RelWithDebInfo build for the in-memory code size of the resulting
// fgfs binary (CODE column in 'top', after selecting a suitable
// unit). If I look at the fgfs binary size (statically-linked with
// SimGear), I measure from 2 KB per template (Release) to 30 KB per
// template (RelWithDebInfo). Finally, a Debug build compiled with
// '-fno-omit-frame-pointer -O0 -fno-inline' lies between the Release
// and the RelWithDebInfo builds.
#if 0
template
signed char readNonNegativeInt<signed char, 10>(const std::string& s);
template
signed char readNonNegativeInt<signed char, 16>(const std::string& s);
template
unsigned char readNonNegativeInt<unsigned char, 10>(const std::string& s);
template
unsigned char readNonNegativeInt<unsigned char, 16>(const std::string& s);
template
short readNonNegativeInt<short, 10>(const std::string& s);
template
short readNonNegativeInt<short, 16>(const std::string& s);
template
unsigned short readNonNegativeInt<unsigned short, 10>(const std::string& s);
template
unsigned short readNonNegativeInt<unsigned short, 16>(const std::string& s);
#endif
template
int readNonNegativeInt<int, 10>(const std::string& s);
template
unsigned int readNonNegativeInt<unsigned int, 10>(const std::string& s);
#if 0
template
int readNonNegativeInt<int, 16>(const std::string& s);
template
unsigned int readNonNegativeInt<unsigned int, 16>(const std::string& s);
template
long readNonNegativeInt<long, 10>(const std::string& s);
template
long readNonNegativeInt<long, 16>(const std::string& s);
template
unsigned long readNonNegativeInt<unsigned long, 10>(const std::string& s);
template
unsigned long readNonNegativeInt<unsigned long, 16>(const std::string& s);
template
long long readNonNegativeInt<long long, 10>(const std::string& s);
template
long long readNonNegativeInt<long long, 16>(const std::string& s);
template
unsigned long long readNonNegativeInt<unsigned long long, 10>(
const std::string& s);
template
unsigned long long readNonNegativeInt<unsigned long long, 16>(
const std::string& s);
#endif
int compare_versions(const string& v1, const string& v2, int maxComponents)
{
vector<string> v1parts(split(v1, "."));

View File

@@ -31,6 +31,7 @@
#include <string>
#include <vector>
#include <type_traits>
#include <cstdlib>
typedef std::vector < std::string > string_list;
@@ -169,6 +170,44 @@ namespace simgear {
*/
int to_int(const std::string& s, int base = 10);
/** Convert a char to the integer it represents in the specified BASE.
*
* Contrary to std::isdigit() and std::isxdigit(), only the standard ASCII
* digits for BASE are accepted (with both uppercase and lowercase 'a'-'f'
* letters for base 16). Throw sg_range_exception if the char is not a
* valid digit for this base.
*
* See template specializations in strutils.cxx.
*/
template<int BASE>
int digitValue(char c);
/** Return:
* - std::string("decimal") if BASE is 10;
* - std::string("hexadecimal") if BASE is 16.
*
* Template specializations in strutils.cxx.
*/
template<int BASE>
std::string numerationBaseAdjective();
/** Convert a string representing an integer to an integral type.
*
* The input string must be non-empty and contain only digits of the
* specified BASE (template parameter). Throw:
* - sg_format_exception if the input string doesn't respect these
* constraints;
* - sg_range_exception if the value can't be represented by type T
* (i.e., if it is too large).
*
* Explicit template instantiations are added as needed in strutils.cxx.
* Have a look there and enable the ones you need!
*/
template<
class T,
int BASE = 10,
typename = typename std::enable_if<std::is_integral<T>::value, T>::type >
T readNonNegativeInt(const std::string& s);
/**
* Convert a string representing a boolean, to a bool.

View File

@@ -6,12 +6,19 @@
#include <vector>
#include <utility> // std::move()
#include <fstream> // std::ifstream
#include <cerrno>
#include <sstream> // std::ostringstream
#include <ios> // std::dec, std::hex
#include <limits> // std::numeric_limits
#include <typeinfo> // typeid()
#include <cstdint> // uint16_t, uintmax_t, etc.
#include <cstdlib> // _set_errno() on Windows
#include <cerrno>
#include <cassert>
#include <simgear/misc/test_macros.hxx>
#include <simgear/compiler.h>
#include <simgear/misc/strutils.hxx>
#include <simgear/structure/exception.hxx>
using std::string;
using std::vector;
@@ -90,6 +97,290 @@ void test_to_int()
SG_CHECK_EQUAL(strutils::to_int("-10000"), -10000);
}
// Auxiliary function for test_readNonNegativeInt()
void aux_readNonNegativeInt_setUpOStringStream(std::ostringstream& oss, int base)
{
switch (base) {
case 10:
oss << std::dec;
break;
case 16:
oss << std::hex;
break;
default:
SG_TEST_FAIL("unsupported value for 'base': " + std::to_string(base));
}
}
// Auxiliary function for test_readNonNegativeInt(): round-trip conversion for
// the given number of values below and up to std::numeric_limits<T>::max().
template<typename T, int BASE>
void aux_readNonNegativeInt_testValuesCloseToMax(T nbValues)
{
std::ostringstream oss;
assert(0 <= nbValues && nbValues <= std::numeric_limits<T>::max());
aux_readNonNegativeInt_setUpOStringStream(oss, BASE);
for (T i = std::numeric_limits<T>::max() - nbValues;
i < std::numeric_limits<T>::max(); i++) {
T valueToTest = i + 1;
T roundTripResult;
bool gotException = false;
oss.str("");
// The cast is only useful when T is a char type
oss << static_cast<uintmax_t>(valueToTest);
try {
roundTripResult = strutils::readNonNegativeInt<T, BASE>(oss.str());
} catch (const sg_range_exception&) {
gotException = true;
}
SG_VERIFY(!gotException);
SG_CHECK_EQUAL(roundTripResult, valueToTest);
}
}
// Auxiliary class for test_readNonNegativeInt(): test that we do get an
// exception when trying to convert the smallest, positive out-of-range value
// for type T.
template<typename T, int BASE>
class ReadNonNegativeInt_JustOutOfRangeTester {
public:
ReadNonNegativeInt_JustOutOfRangeTester()
{ }
// Run the test
void run()
{
std::ostringstream oss;
aux_readNonNegativeInt_setUpOStringStream(oss, BASE);
oss << 1 + static_cast<uintmax_t>(std::numeric_limits<T>::max());
bool gotException = false;
try {
strutils::readNonNegativeInt<T, BASE>(oss.str());
} catch (const sg_range_exception&) {
gotException = true;
}
SG_VERIFY(gotException);
}
};
class ReadNonNegativeInt_DummyTester {
public:
ReadNonNegativeInt_DummyTester()
{ }
void run()
{ }
};
// We use this helper class to automatically determine for which types
// ReadNonNegativeInt_JustOutOfRangeTester::run() can be run.
template<typename T, int BASE>
class AuxReadNonNegativeInt_JustOutOfRange_Helper
{
typedef typename std::make_unsigned<T>::type uT;
// Define TestRunner to be either
//
// ReadNonNegativeInt_JustOutOfRangeTester<T, BASE>
//
// or
//
// ReadNonNegativeInt_DummyTester
//
// depending on whether 1 + std::numeric_limits<T>::max() can be
// represented by uintmax_t.
typedef typename std::conditional<
static_cast<uT>(std::numeric_limits<T>::max()) <
std::numeric_limits<uintmax_t>::max(),
ReadNonNegativeInt_JustOutOfRangeTester<T, BASE>,
ReadNonNegativeInt_DummyTester >::type TestRunner;
public:
AuxReadNonNegativeInt_JustOutOfRange_Helper()
{ };
void test()
{
TestRunner().run();
}
};
void test_readNonNegativeInt()
{
// In order to save some bytes for the SimGearCore library[*], we only
// instantiated a small number of variants of readNonNegativeInt() in
// strutils.cxx. This is why many tests are disabled with '#if 0' below. Of
// course, more variants can be enabled when they are needed.
//
// [*] See measures in strutils.cxx before the template instantiations.
#if 0
SG_CHECK_EQUAL((strutils::readNonNegativeInt<short>("0")), 0);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<short>("23")), 23);
#endif
SG_CHECK_EQUAL((strutils::readNonNegativeInt<int>("0")), 0);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<int>("00000000")), 0);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<int>("12345")), 12345);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<int, 10>("12345")), 12345);
#if 0
SG_CHECK_EQUAL((strutils::readNonNegativeInt<int, 16>("ff")), 0xff);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<int, 16>("a5E9")), 0xa5e9);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<unsigned long, 16>("0cda")),
0x0cda);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<uint16_t, 10>("65535")), 0xffff);
SG_CHECK_EQUAL(
(strutils::readNonNegativeInt<uint16_t, 10>("00000000000000000000065535")),
0xffff);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<uint16_t, 16>("ffff")), 0xffff);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<int16_t, 10>("32767")), 0x7fff);
SG_CHECK_EQUAL((strutils::readNonNegativeInt<int16_t, 16>("7fff")), 0x7fff);
#endif
// Nothing special about the values :)
SG_CHECK_EQUAL_NOSTREAM((typeid(strutils::readNonNegativeInt<int, 10>("72"))),
typeid(int(12)));
#if 0
SG_CHECK_EQUAL_NOSTREAM((typeid(strutils::readNonNegativeInt<long, 10>("72"))),
typeid(12L));
SG_CHECK_EQUAL_NOSTREAM(
(typeid(strutils::readNonNegativeInt<long long, 10>("72"))),
typeid(12LL));
#endif
{
bool gotException = false;
try {
strutils::readNonNegativeInt<int>(""); // empty string: illegal
} catch (const sg_format_exception&) {
gotException = true;
}
SG_VERIFY(gotException);
}
{
bool gotException = false;
try {
strutils::readNonNegativeInt<int>("-1"); // non-digit character: illegal
} catch (const sg_format_exception&) {
gotException = true;
}
SG_VERIFY(gotException);
}
{
bool gotException = false;
try {
strutils::readNonNegativeInt<int>("+1"); // non-digit character: illegal
} catch (const sg_format_exception&) {
gotException = true;
}
SG_VERIFY(gotException);
}
{
bool gotException = false;
try {
strutils::readNonNegativeInt<int>("858efe"); // trailing garbage: illegal
} catch (const sg_format_exception&) {
gotException = true;
}
SG_VERIFY(gotException);
}
#if 0
{
bool gotException = false;
try {
strutils::readNonNegativeInt<int, 16>("858g5k"); // ditto for base 16
} catch (const sg_format_exception&) {
gotException = true;
}
SG_VERIFY(gotException);
}
#endif
{
bool gotException = false;
try {
strutils::readNonNegativeInt<int>(" 858"); // leading whitespace/garbage:
} catch (const sg_format_exception&) { // illegal too
gotException = true;
}
SG_VERIFY(gotException);
}
// Try to read a value that is 1 unit too large for the type. Check that it
// raises an sg_range_exception in each case.
#if 0
AuxReadNonNegativeInt_JustOutOfRange_Helper<signed char, 10>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<signed char, 16>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned char, 10>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned char, 16>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<short, 10>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<short, 16>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned short, 10>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned short, 16>().test();
#endif
AuxReadNonNegativeInt_JustOutOfRange_Helper<int, 10>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned int, 10>().test();
#if 0
AuxReadNonNegativeInt_JustOutOfRange_Helper<int, 16>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned int, 16>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<long, 10>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<long, 16>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned long, 10>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned long, 16>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<long long, 10>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<long long, 16>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned long long, 10>().test();
AuxReadNonNegativeInt_JustOutOfRange_Helper<unsigned long long, 16>().test();
#endif
// Round trip tests with large values, including the largest value that can
// be represented by the type, in each case.
//
// Can be casted as any of the following types
constexpr int nbValues = 5000;
#if 0
aux_readNonNegativeInt_testValuesCloseToMax<signed char, 10>(127);
aux_readNonNegativeInt_testValuesCloseToMax<signed char, 16>(127);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned char, 10>(127);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned char, 16>(127);
aux_readNonNegativeInt_testValuesCloseToMax<short, 10>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<short, 16>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned short, 10>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned short, 16>(nbValues);
#endif
aux_readNonNegativeInt_testValuesCloseToMax<int, 10>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned int, 10>(nbValues);
#if 0
aux_readNonNegativeInt_testValuesCloseToMax<int, 16>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned int, 16>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<long, 10>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<long, 16>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned long, 10>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned long, 16>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<long long, 10>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<long long, 16>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned long long, 10>(nbValues);
aux_readNonNegativeInt_testValuesCloseToMax<unsigned long long, 16>(nbValues);
#endif
}
void test_split()
{
string_list l = strutils::split("zero one two three four five");
@@ -299,6 +590,7 @@ int main(int argc, char* argv[])
test_ends_with();
test_simplify();
test_to_int();
test_readNonNegativeInt();
test_split();
test_escape();
test_unescape();