Я знаю, что это более старый вопрос, но я сталкивался с ним много раз и до сих пор не нашел решения с хорошими шаблонами, имеющего следующие характеристики:
- Может конвертировать любую базу (и определять тип базы)
- Обнаружит ошибочные данные (т. Е. Обеспечит, чтобы вся строка, за исключением начальных / конечных пробелов, использовалась преобразованием)
- Гарантирует, что независимо от типа, преобразованного в, диапазон значения строки является приемлемым.
Итак, вот мой, с тестовым ремешком. Поскольку он использует функции C strtoull / strtoll под капотом, он всегда сначала конвертируется в самый большой доступный тип. Затем, если вы не используете самый большой тип, он выполнит дополнительные проверки диапазона, чтобы убедиться, что ваш тип не был переполнен. Для этого он немного менее эффективен, чем если бы вы правильно выбрали strtol / strtoul. Тем не менее, он также работает для шортов / символов и, насколько мне известно, не существует стандартной библиотечной функции, которая бы это тоже выполняла.
Наслаждайтесь; надеюсь, кто-то найдет это полезным.
#include <cstdlib>
#include <cerrno>
#include <limits>
#include <stdexcept>
#include <sstream>
static const int DefaultBase = 10;
template<typename T>
static inline T CstrtoxllWrapper(const char *str, int base = DefaultBase)
{
while (isspace(*str)) str++; // remove leading spaces; verify there's data
if (*str == '\0') { throw std::invalid_argument("str; no data"); } // nothing to convert
// NOTE: for some reason strtoull allows a negative sign, we don't; if
// converting to an unsigned then it must always be positive!
if (!std::numeric_limits<T>::is_signed && *str == '-')
{ throw std::invalid_argument("str; negative"); }
// reset errno and call fn (either strtoll or strtoull)
errno = 0;
char *ePtr;
T tmp = std::numeric_limits<T>::is_signed ? strtoll(str, &ePtr, base)
: strtoull(str, &ePtr, base);
// check for any C errors -- note these are range errors on T, which may
// still be out of the range of the actual type we're using; the caller
// may need to perform additional range checks.
if (errno != 0)
{
if (errno == ERANGE) { throw std::range_error("str; out of range"); }
else if (errno == EINVAL) { throw std::invalid_argument("str; EINVAL"); }
else { throw std::invalid_argument("str; unknown errno"); }
}
// verify everything converted -- extraneous spaces are allowed
if (ePtr != NULL)
{
while (isspace(*ePtr)) ePtr++;
if (*ePtr != '\0') { throw std::invalid_argument("str; bad data"); }
}
return tmp;
}
template<typename T>
T StringToSigned(const char *str, int base = DefaultBase)
{
static const long long max = std::numeric_limits<T>::max();
static const long long min = std::numeric_limits<T>::min();
long long tmp = CstrtoxllWrapper<typeof(tmp)>(str, base); // use largest type
// final range check -- only needed if not long long type; a smart compiler
// should optimize this whole thing out
if (sizeof(T) == sizeof(tmp)) { return tmp; }
if (tmp < min || tmp > max)
{
std::ostringstream err;
err << "str; value " << tmp << " out of " << sizeof(T) * 8
<< "-bit signed range (";
if (sizeof(T) != 1) err << min << ".." << max;
else err << (int) min << ".." << (int) max; // don't print garbage chars
err << ")";
throw std::range_error(err.str());
}
return tmp;
}
template<typename T>
T StringToUnsigned(const char *str, int base = DefaultBase)
{
static const unsigned long long max = std::numeric_limits<T>::max();
unsigned long long tmp = CstrtoxllWrapper<typeof(tmp)>(str, base); // use largest type
// final range check -- only needed if not long long type; a smart compiler
// should optimize this whole thing out
if (sizeof(T) == sizeof(tmp)) { return tmp; }
if (tmp > max)
{
std::ostringstream err;
err << "str; value " << tmp << " out of " << sizeof(T) * 8
<< "-bit unsigned range (0..";
if (sizeof(T) != 1) err << max;
else err << (int) max; // don't print garbage chars
err << ")";
throw std::range_error(err.str());
}
return tmp;
}
template<typename T>
inline T
StringToDecimal(const char *str, int base = DefaultBase)
{
return std::numeric_limits<T>::is_signed ? StringToSigned<T>(str, base)
: StringToUnsigned<T>(str, base);
}
template<typename T>
inline T
StringToDecimal(T &out_convertedVal, const char *str, int base = DefaultBase)
{
return out_convertedVal = StringToDecimal<T>(str, base);
}
/*============================== [ Test Strap ] ==============================*/
#include <inttypes.h>
#include <iostream>
static bool _g_anyFailed = false;
template<typename T>
void TestIt(const char *tName,
const char *s, int base,
bool successExpected = false, T expectedValue = 0)
{
#define FAIL(s) { _g_anyFailed = true; std::cout << s; }
T x;
std::cout << "converting<" << tName << ">b:" << base << " [" << s << "]";
try
{
StringToDecimal<T>(x, s, base);
// get here on success only
if (!successExpected)
{
FAIL(" -- TEST FAILED; SUCCESS NOT EXPECTED!" << std::endl);
}
else
{
std::cout << " -> ";
if (sizeof(T) != 1) std::cout << x;
else std::cout << (int) x; // don't print garbage chars
if (x != expectedValue)
{
FAIL("; FAILED (expected value:" << expectedValue << ")!");
}
std::cout << std::endl;
}
}
catch (std::exception &e)
{
if (successExpected)
{
FAIL( " -- TEST FAILED; EXPECTED SUCCESS!"
<< " (got:" << e.what() << ")" << std::endl);
}
else
{
std::cout << "; expected exception encounterd: [" << e.what() << "]" << std::endl;
}
}
}
#define TEST(t, s, ...) \
TestIt<t>(#t, s, __VA_ARGS__);
int main()
{
std::cout << "============ variable base tests ============" << std::endl;
TEST(int, "-0xF", 0, true, -0xF);
TEST(int, "+0xF", 0, true, 0xF);
TEST(int, "0xF", 0, true, 0xF);
TEST(int, "-010", 0, true, -010);
TEST(int, "+010", 0, true, 010);
TEST(int, "010", 0, true, 010);
TEST(int, "-10", 0, true, -10);
TEST(int, "+10", 0, true, 10);
TEST(int, "10", 0, true, 10);
std::cout << "============ base-10 tests ============" << std::endl;
TEST(int, "-010", 10, true, -10);
TEST(int, "+010", 10, true, 10);
TEST(int, "010", 10, true, 10);
TEST(int, "-10", 10, true, -10);
TEST(int, "+10", 10, true, 10);
TEST(int, "10", 10, true, 10);
TEST(int, "00010", 10, true, 10);
std::cout << "============ base-8 tests ============" << std::endl;
TEST(int, "777", 8, true, 0777);
TEST(int, "-0111 ", 8, true, -0111);
TEST(int, "+0010 ", 8, true, 010);
std::cout << "============ base-16 tests ============" << std::endl;
TEST(int, "DEAD", 16, true, 0xDEAD);
TEST(int, "-BEEF", 16, true, -0xBEEF);
TEST(int, "+C30", 16, true, 0xC30);
std::cout << "============ base-2 tests ============" << std::endl;
TEST(int, "-10011001", 2, true, -153);
TEST(int, "10011001", 2, true, 153);
std::cout << "============ irregular base tests ============" << std::endl;
TEST(int, "Z", 36, true, 35);
TEST(int, "ZZTOP", 36, true, 60457993);
TEST(int, "G", 17, true, 16);
TEST(int, "H", 17);
std::cout << "============ space deliminated tests ============" << std::endl;
TEST(int, "1337 ", 10, true, 1337);
TEST(int, " FEAD", 16, true, 0xFEAD);
TEST(int, " 0711 ", 0, true, 0711);
std::cout << "============ bad data tests ============" << std::endl;
TEST(int, "FEAD", 10);
TEST(int, "1234 asdfklj", 10);
TEST(int, "-0xF", 10);
TEST(int, "+0xF", 10);
TEST(int, "0xF", 10);
TEST(int, "-F", 10);
TEST(int, "+F", 10);
TEST(int, "12.4", 10);
TEST(int, "ABG", 16);
TEST(int, "10011002", 2);
std::cout << "============ int8_t range tests ============" << std::endl;
TEST(int8_t, "7F", 16, true, std::numeric_limits<int8_t>::max());
TEST(int8_t, "80", 16);
TEST(int8_t, "-80", 16, true, std::numeric_limits<int8_t>::min());
TEST(int8_t, "-81", 16);
TEST(int8_t, "FF", 16);
TEST(int8_t, "100", 16);
std::cout << "============ uint8_t range tests ============" << std::endl;
TEST(uint8_t, "7F", 16, true, std::numeric_limits<int8_t>::max());
TEST(uint8_t, "80", 16, true, std::numeric_limits<int8_t>::max()+1);
TEST(uint8_t, "-80", 16);
TEST(uint8_t, "-81", 16);
TEST(uint8_t, "FF", 16, true, std::numeric_limits<uint8_t>::max());
TEST(uint8_t, "100", 16);
std::cout << "============ int16_t range tests ============" << std::endl;
TEST(int16_t, "7FFF", 16, true, std::numeric_limits<int16_t>::max());
TEST(int16_t, "8000", 16);
TEST(int16_t, "-8000", 16, true, std::numeric_limits<int16_t>::min());
TEST(int16_t, "-8001", 16);
TEST(int16_t, "FFFF", 16);
TEST(int16_t, "10000", 16);
std::cout << "============ uint16_t range tests ============" << std::endl;
TEST(uint16_t, "7FFF", 16, true, std::numeric_limits<int16_t>::max());
TEST(uint16_t, "8000", 16, true, std::numeric_limits<int16_t>::max()+1);
TEST(uint16_t, "-8000", 16);
TEST(uint16_t, "-8001", 16);
TEST(uint16_t, "FFFF", 16, true, std::numeric_limits<uint16_t>::max());
TEST(uint16_t, "10000", 16);
std::cout << "============ int32_t range tests ============" << std::endl;
TEST(int32_t, "7FFFFFFF", 16, true, std::numeric_limits<int32_t>::max());
TEST(int32_t, "80000000", 16);
TEST(int32_t, "-80000000", 16, true, std::numeric_limits<int32_t>::min());
TEST(int32_t, "-80000001", 16);
TEST(int32_t, "FFFFFFFF", 16);
TEST(int32_t, "100000000", 16);
std::cout << "============ uint32_t range tests ============" << std::endl;
TEST(uint32_t, "7FFFFFFF", 16, true, std::numeric_limits<int32_t>::max());
TEST(uint32_t, "80000000", 16, true, std::numeric_limits<int32_t>::max()+1);
TEST(uint32_t, "-80000000", 16);
TEST(uint32_t, "-80000001", 16);
TEST(uint32_t, "FFFFFFFF", 16, true, std::numeric_limits<uint32_t>::max());
TEST(uint32_t, "100000000", 16);
std::cout << "============ int64_t range tests ============" << std::endl;
TEST(int64_t, "7FFFFFFFFFFFFFFF", 16, true, std::numeric_limits<int64_t>::max());
TEST(int64_t, "8000000000000000", 16);
TEST(int64_t, "-8000000000000000", 16, true, std::numeric_limits<int64_t>::min());
TEST(int64_t, "-8000000000000001", 16);
TEST(int64_t, "FFFFFFFFFFFFFFFF", 16);
TEST(int64_t, "10000000000000000", 16);
std::cout << "============ uint64_t range tests ============" << std::endl;
TEST(uint64_t, "7FFFFFFFFFFFFFFF", 16, true, std::numeric_limits<int64_t>::max());
TEST(uint64_t, "8000000000000000", 16, true, std::numeric_limits<int64_t>::max()+1);
TEST(uint64_t, "-8000000000000000", 16);
TEST(uint64_t, "-8000000000000001", 16);
TEST(uint64_t, "FFFFFFFFFFFFFFFF", 16, true, std::numeric_limits<uint64_t>::max());
TEST(uint64_t, "10000000000000000", 16);
std::cout << std::endl << std::endl
<< (_g_anyFailed ? "!! SOME TESTS FAILED !!" : "ALL TESTS PASSED")
<< std::endl;
return _g_anyFailed;
}
StringToDecimal
- метод пользовательской земли; он перегружен, поэтому его можно назвать так:
int a; a = StringToDecimal<int>("100");
или это:
int a; StringToDecimal(a, "100");
Я ненавижу повторять тип int, поэтому предпочитаю последний. Это гарантирует, что если тип «а» изменится, он не получит плохих результатов. Я хотел бы, чтобы компилятор мог понять это как:
int a; a = StringToDecimal("100");
... но C ++ не выводит типы возвращаемых шаблонов, так что это лучшее, что я могу получить.
Реализация довольно проста:
CstrtoxllWrapper
включает в себя оба strtoull
и strtoll
, вызывая их в зависимости от подписи типа шаблона и предоставляя некоторые дополнительные гарантии (например, отрицательный ввод запрещен, если он не подписан, и обеспечивает преобразование всей строки) .
CstrtoxllWrapper
используется StringToSigned
и StringToUnsigned
с наибольшим типом (long long / unsigned long long), доступным для компилятора; это позволяет выполнить максимальное преобразование. Затем, если это необходимо, StringToSigned
/ StringToUnsigned
выполняет окончательную проверку диапазона базового типа. Наконец, метод конечной точки, StringToDecimal
, решает, какой из шаблонных методов StringTo * вызывать на основе подписи базового типа.
Я думаю, что большая часть мусора может быть оптимизирована компилятором; почти все должно быть детерминированным во время компиляции. Любой комментарий по этому аспекту будет мне интересен!