Skip to content

Commit 0c6884e

Browse files
authored
Merge pull request #76 from kaitai-io/to_string-simplify
to_string(): replace template with 6 overloads, simplify
2 parents dbe1ca1 + cb1c7b1 commit 0c6884e

File tree

7 files changed

+293
-95
lines changed

7 files changed

+293
-95
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
diff --git a/googletest/src/gtest-death-test.cc b/googletest/src/gtest-death-test.cc
2+
index 0908355..d6f492e 100644
3+
--- a/googletest/src/gtest-death-test.cc
4+
+++ b/googletest/src/gtest-death-test.cc
5+
@@ -1212,14 +1212,14 @@ static int ExecDeathTestChildMain(void* child_arg) {
6+
static void StackLowerThanAddress(const void* ptr,
7+
bool* result) GTEST_NO_INLINE_;
8+
static void StackLowerThanAddress(const void* ptr, bool* result) {
9+
- int dummy;
10+
+ int dummy = 0;
11+
*result = (&dummy < ptr);
12+
}
13+
14+
// Make sure AddressSanitizer does not tamper with the stack here.
15+
GTEST_ATTRIBUTE_NO_SANITIZE_ADDRESS_
16+
static bool StackGrowsDown() {
17+
- int dummy;
18+
+ int dummy = 0;
19+
bool result;
20+
StackLowerThanAddress(&dummy, &result);
21+
return result;

.build/install-gtest

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,51 @@ set -ef
55
#
66
# See https://github.com/google/googletest/blob/d83fee138a9ae6cb7c03688a2d08d4043a39815d/googletest/README.md#build-with-cmake
77

8-
gtest_version_tag=v1.14.0
8+
# https://unix.stackexchange.com/a/706411
9+
script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
10+
11+
usage()
12+
{
13+
echo "Usage: $0 <cpp-standard>"
14+
echo
15+
echo ' <cpp-standard> C++ standard to target (supported: 98, 11, any number between 14 and 26 inclusive)'
16+
}
17+
18+
die()
19+
{
20+
printf '%s\n' "$1" >&2
21+
printf '\n%s\n' "$(usage)" >&2
22+
exit 1
23+
}
24+
25+
cpp_standard=$1
26+
if [ -z "$cpp_standard" ]; then
27+
die 'Error: <cpp-standard> must not be empty'
28+
fi
29+
30+
case $cpp_standard in
31+
98)
32+
# GoogleTest v1.8.1 seems to be the last version supporting C++98,
33+
# see https://github.com/google/googletest/releases/tag/release-1.8.1
34+
gtest_version_tag=release-1.8.1
35+
;;
36+
11)
37+
# GoogleTest v1.12.1 seems to be the last version supporting C++11,
38+
# see https://github.com/google/googletest/releases/tag/release-1.12.1
39+
gtest_version_tag=release-1.12.1
40+
;;
41+
*)
42+
if [ "$cpp_standard" -ge 14 ] && [ "$cpp_standard" -le 26 ]; then
43+
# GoogleTest v1.16.0 (the latest version at the time of writing) requires at
44+
# least C++14, see https://github.com/google/googletest/releases/tag/v1.16.0
45+
gtest_version_tag=v1.16.0
46+
else
47+
die "Error: unsupported value $1 of <cpp-standard>"
48+
fi
49+
;;
50+
esac
51+
52+
echo "Installing GoogleTest $gtest_version_tag"
953

1054
temp_dir=$(mktemp -d)
1155
cleanup()
@@ -20,6 +64,11 @@ trap 'cleanup' INT TERM HUP EXIT
2064
# Download
2165
git clone --depth 1 -b "$gtest_version_tag" -- https://github.com/google/googletest.git "$temp_dir"
2266
cd -- "$temp_dir"
67+
if [ "$gtest_version_tag" = release-1.8.1 ]; then
68+
# We must apply a patch to fix `-Werror=maybe-uninitialized` (see
69+
# https://github.com/google/googletest/issues/3219), otherwise it won't compile
70+
git apply -- "$script_dir"/gtest-v1.8.1-fix-Werror-maybe-uninitialized.patch
71+
fi
2372
mkdir build
2473
cd build
2574
# Configure - build only GoogleTest, not GoogleMock

.github/workflows/build.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ jobs:
1919
steps:
2020
- uses: actions/checkout@v4
2121
- name: Install GoogleTest
22-
run: .build/install-gtest
22+
env:
23+
CPP_STANDARD: ${{ matrix.cpp-standard }}
24+
run: .build/install-gtest "$CPP_STANDARD"
2325
- name: Update package lists
2426
run: sudo apt-get update
2527
- name: Install packages
@@ -123,7 +125,7 @@ jobs:
123125
pkg info
124126
125127
echo '#### Installing GoogleTest'
126-
.build/install-gtest
128+
.build/install-gtest 11
127129
128130
echo '#### Building'
129131
.build/build -DCMAKE_CXX_STANDARD=11 -DCMAKE_CXX_STANDARD_REQUIRED=ON -DCMAKE_CXX_EXTENSIONS=OFF

kaitai/kaitaistream.cpp

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -791,20 +791,66 @@ int kaitai::kstream::mod(int a, int b) {
791791
return r;
792792
}
793793

794-
void kaitai::kstream::unsigned_to_decimal(uint64_t number, char *buffer) {
795-
// Implementation from https://ideone.com/nrQfA8 by Alf P. Steinbach
794+
void kaitai::kstream::unsigned_to_decimal(uint64_t number, char *buf, std::size_t &buf_contents_start) {
795+
// Implementation inspired by https://ideone.com/nrQfA8 by Alf P. Steinbach
796796
// (see https://vitaut.net/posts/2013/integer-to-string-conversion-in-cplusplus/)
797-
if (number == 0) {
798-
*buffer++ = '0';
797+
do {
798+
buf[--buf_contents_start] = static_cast<char>('0' + (number % 10));
799+
number /= 10;
800+
} while (number != 0);
801+
}
802+
803+
std::string kaitai::kstream::to_string_signed(int64_t val) {
804+
// `digits10 + 2` because of minus sign + leading digit (NB: null terminator is not used)
805+
char buf[std::numeric_limits<int64_t>::digits10 + 2];
806+
std::size_t buf_contents_start = sizeof(buf);
807+
if (val < 0) {
808+
// NB: `val` is negative and we need to get its absolute value (i.e. minus `val`). However, since
809+
// `int64_t` uses two's complement representation, its range is `[-2**63, 2**63 - 1] =
810+
// [-0x8000_0000_0000_0000, 0x7fff_ffff_ffff_ffff]` (both ends inclusive) and thus the naive
811+
// `-val` operation will overflow for `val = std::numeric_limits<int64_t>::min() =
812+
// -0x8000_0000_0000_0000` (because the result of `-val` is mathematically
813+
// `-(-0x8000_0000_0000_0000) = 0x8000_0000_0000_0000`, but the `int64_t` type can represent at
814+
// most `0x7fff_ffff_ffff_ffff`). And signed integer overflow is undefined behavior in C++.
815+
//
816+
// To avoid undefined behavior for `val = -0x8000_0000_0000_0000 = -2**63`, we do the following
817+
// steps for all negative `val`s:
818+
//
819+
// 1. Convert the signed (and negative) `val` to an unsigned `uint64_t` type. This is a
820+
// well-defined operation in C++: the resulting `uint64_t` value will be `val mod 2**64` (`mod`
821+
// is modulo). The maximum `val` we can have here is `-1` (because `val < 0`), a theoretical
822+
// minimum we are able to support would be `-2**64 + 1 = -0xffff_ffff_ffff_ffff` (even though
823+
// in practice the widest standard type is `int64_t` with the minimum of `-2**63`):
824+
//
825+
// * `static_cast<uint64_t>(-1) = -1 mod 2**64 = 2**64 + (-1) = 0xffff_ffff_ffff_ffff = 2**64 - 1`
826+
// * `static_cast<uint64_t>(-2**64 + 1) = (-2**64 + 1) mod 2**64 = 2**64 + (-2**64 + 1) = 1`
827+
//
828+
// 2. Subtract `static_cast<uint64_t>(val)` from `2**64 - 1 = 0xffff_ffff_ffff_ffff`. Since
829+
// `static_cast<uint64_t>(val)` is in range `[1, 2**64 - 1]` (see step 1), the result of this
830+
// subtraction will be mathematically in range `[0, (2**64 - 1) - 1] = [0, 2**64 - 2]`. So the
831+
// mathematical result cannot be negative, hence this unsigned integer subtraction can never
832+
// wrap around (which wouldn't be a good thing to rely upon because it confuses programmers and
833+
// code analysis tools).
834+
//
835+
// 3. Since we did mathematically `(2**64 - 1) - (2**64 + val) = -val - 1` so far (and we wanted
836+
// to do `-val`), we add `1` to correct that. From step 2 we know that the result of `-val - 1`
837+
// is in range `[0, 2**64 - 2]`, so adding `1` will not wrap (at most we could get `2**64 - 1 =
838+
// 0xffff_ffff_ffff_ffff`, which is still in the valid range of `uint64_t`).
839+
unsigned_to_decimal((std::numeric_limits<uint64_t>::max() - static_cast<uint64_t>(val)) + 1, buf, buf_contents_start);
840+
841+
buf[--buf_contents_start] = '-';
799842
} else {
800-
char *p_first = buffer;
801-
while (number != 0) {
802-
*buffer++ = static_cast<char>('0' + number % 10);
803-
number /= 10;
804-
}
805-
std::reverse(p_first, buffer);
843+
unsigned_to_decimal(static_cast<uint64_t>(val), buf, buf_contents_start);
806844
}
807-
*buffer = '\0';
845+
return std::string(&buf[buf_contents_start], sizeof(buf) - buf_contents_start);
846+
}
847+
848+
std::string kaitai::kstream::to_string_unsigned(uint64_t val) {
849+
// `digits10 + 1` because of leading digit (NB: null terminator is not used)
850+
char buf[std::numeric_limits<uint64_t>::digits10 + 1];
851+
std::size_t buf_contents_start = sizeof(buf);
852+
unsigned_to_decimal(val, buf, buf_contents_start);
853+
return std::string(&buf[buf_contents_start], sizeof(buf) - buf_contents_start);
808854
}
809855

810856
int64_t kaitai::kstream::string_to_int(const std::string& str, int base) {

kaitai/kaitaistream.h

Lines changed: 62 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,10 @@
1212
#include <stdint.h> // int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t
1313

1414
#include <ios> // std::streamsize, forward declaration of std::istream // IWYU pragma: keep
15-
#include <limits> // std::numeric_limits
15+
#include <cstddef> // std::size_t
1616
#include <sstream> // std::istringstream // IWYU pragma: keep
1717
#include <string> // std::string
1818

19-
#ifdef KAITAI_STREAM_H_CPP11_SUPPORT
20-
#include <type_traits> // std::enable_if, std::is_integral
21-
#endif
22-
2319
namespace kaitai {
2420

2521
/**
@@ -232,70 +228,72 @@ class kstream {
232228
*/
233229
static int mod(int a, int b);
234230

231+
// NB: the following 6 overloads of `to_string` are exactly the ones that
232+
// [`std::to_string`](https://en.cppreference.com/w/cpp/string/basic_string/to_string) has.
233+
// Testing has shown that they are all necessary: if you remove any of them, you will get
234+
// something like `error: call to 'to_string' is ambiguous` when trying to call `to_string`
235+
// with the integer type for which you removed the overload.
236+
237+
/**
238+
* Converts given integer `val` to a decimal string representation.
239+
* Should be used in place of `std::to_string(int)` (which is available only
240+
* since C++11) in older C++ implementations.
241+
*/
242+
static std::string to_string(int val) {
243+
return to_string_signed(val);
244+
}
245+
235246
/**
236247
* Converts given integer `val` to a decimal string representation.
237-
* Should be used in place of std::to_string() (which is available only
248+
* Should be used in place of `std::to_string(long)` (which is available only
238249
* since C++11) in older C++ implementations.
239250
*/
240-
template<typename I>
251+
static std::string to_string(long val) {
252+
return to_string_signed(val);
253+
}
254+
255+
// The `long long` type is only available since C++11, so we use it only in C++11 mode.
241256
#ifdef KAITAI_STREAM_H_CPP11_SUPPORT
242-
// https://stackoverflow.com/a/27913885
243-
typename std::enable_if<
244-
std::is_integral<I>::value &&
245-
// check if we don't have something too large like GCC's `__int128_t`
246-
std::numeric_limits<I>::max() >= 0 &&
247-
std::numeric_limits<I>::max() <= std::numeric_limits<uint64_t>::max(),
248-
std::string
249-
>::type
250-
#else
251-
std::string
257+
/**
258+
* Converts given integer `val` to a decimal string representation.
259+
* Should be used in place of `std::to_string(long long)` (which is available only
260+
* since C++11) in older C++ implementations.
261+
*/
262+
static std::string to_string(long long val) {
263+
return to_string_signed(val);
264+
}
252265
#endif
253-
static to_string(I val) {
254-
// in theory, `digits10 + 3` would be enough (minus sign + leading digit
255-
// + null terminator), but let's add a little more to be safe
256-
char buf[std::numeric_limits<I>::digits10 + 5];
257-
if (val < 0) {
258-
buf[0] = '-';
259-
260-
// NB: `val` is negative and we need to get its absolute value (i.e. minus `val`). However, since
261-
// `int64_t` uses two's complement representation, its range is `[-2**63, 2**63 - 1] =
262-
// [-0x8000_0000_0000_0000, 0x7fff_ffff_ffff_ffff]` (both ends inclusive) and thus the naive
263-
// `-val` operation will overflow for `val = std::numeric_limits<int64_t>::min() =
264-
// -0x8000_0000_0000_0000` (because the result of `-val` is mathematically
265-
// `-(-0x8000_0000_0000_0000) = 0x8000_0000_0000_0000`, but the `int64_t` type can represent at
266-
// most `0x7fff_ffff_ffff_ffff`). And signed integer overflow is undefined behavior in C++.
267-
//
268-
// To avoid undefined behavior for `val = -0x8000_0000_0000_0000 = -2**63`, we do the following
269-
// steps for all negative `val`s:
270-
//
271-
// 1. Convert the signed (and negative) `val` to an unsigned `uint64_t` type. This is a
272-
// well-defined operation in C++: the resulting `uint64_t` value will be `val mod 2**64` (`mod`
273-
// is modulo). The maximum `val` we can have here is `-1` (because `val < 0`), a theoretical
274-
// minimum we are able to support would be `-2**64 + 1 = -0xffff_ffff_ffff_ffff` (even though
275-
// in practice the widest standard type is `int64_t` with the minimum of `-2**63`):
276-
//
277-
// * `static_cast<uint64_t>(-1) = -1 mod 2**64 = 2**64 + (-1) = 0xffff_ffff_ffff_ffff = 2**64 - 1`
278-
// * `static_cast<uint64_t>(-2**64 + 1) = (-2**64 + 1) mod 2**64 = 2**64 + (-2**64 + 1) = 1`
279-
//
280-
// 2. Subtract `static_cast<uint64_t>(val)` from `2**64 - 1 = 0xffff_ffff_ffff_ffff`. Since
281-
// `static_cast<uint64_t>(val)` is in range `[1, 2**64 - 1]` (see step 1), the result of this
282-
// subtraction will be mathematically in range `[0, (2**64 - 1) - 1] = [0, 2**64 - 2]`. So the
283-
// mathematical result cannot be negative, hence this unsigned integer subtraction can never
284-
// wrap around (which wouldn't be a good thing to rely upon because it confuses programmers and
285-
// code analysis tools).
286-
//
287-
// 3. Since we did mathematically `(2**64 - 1) - (2**64 + val) = -val - 1` so far (and we wanted
288-
// to do `-val`), we add `1` to correct that. From step 2 we know that the result of `-val - 1`
289-
// is in range `[0, 2**64 - 2]`, so adding `1` will not wrap (at most we could get `2**64 - 1 =
290-
// 0xffff_ffff_ffff_ffff`, which is still in the valid range of `uint64_t`).
291-
292-
unsigned_to_decimal((std::numeric_limits<uint64_t>::max() - static_cast<uint64_t>(val)) + 1, &buf[1]);
293-
} else {
294-
unsigned_to_decimal(val, buf);
295-
}
296-
return std::string(buf);
266+
267+
/**
268+
* Converts given integer `val` to a decimal string representation.
269+
* Should be used in place of `std::to_string(unsigned)` (which is available only
270+
* since C++11) in older C++ implementations.
271+
*/
272+
static std::string to_string(unsigned val) {
273+
return to_string_unsigned(val);
274+
}
275+
276+
/**
277+
* Converts given integer `val` to a decimal string representation.
278+
* Should be used in place of `std::to_string(unsigned long)` (which is available only
279+
* since C++11) in older C++ implementations.
280+
*/
281+
static std::string to_string(unsigned long val) {
282+
return to_string_unsigned(val);
297283
}
298284

285+
// The `unsigned long long` type is only available since C++11, so we use it only in C++11 mode.
286+
#ifdef KAITAI_STREAM_H_CPP11_SUPPORT
287+
/**
288+
* Converts given integer `val` to a decimal string representation.
289+
* Should be used in place of `std::to_string(unsigned long long)` (which is available only
290+
* since C++11) in older C++ implementations.
291+
*/
292+
static std::string to_string(unsigned long long val) {
293+
return to_string_unsigned(val);
294+
}
295+
#endif
296+
299297
/**
300298
* Converts string `str` to an integer value. Throws an exception if the
301299
* string is not a valid integer.
@@ -347,7 +345,9 @@ class kstream {
347345
void init();
348346
void exceptions_enable() const;
349347

350-
static void unsigned_to_decimal(uint64_t number, char *buffer);
348+
static void unsigned_to_decimal(uint64_t number, char *buf, std::size_t &buf_contents_start);
349+
static std::string to_string_signed(int64_t val);
350+
static std::string to_string_unsigned(uint64_t val);
351351

352352
#ifdef KS_STR_ENCODING_WIN32API
353353
enum {

tests/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ include(../Common.cmake)
88

99
target_include_directories(unittest PRIVATE ${CMAKE_SOURCE_DIR})
1010

11+
target_compile_options(unittest PRIVATE
12+
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:
13+
# At the time of writing, we use a variadic `SETUP_STREAM(...)` macro everywhere
14+
# in `unittest.cpp`. However, variadic macros are only available since C++11, so
15+
# the `#define SETUP_STREAM(...)` line raises a warning `anonymous variadic macros
16+
# were introduced in C++11` when compiling with `-std=c++98`. Since we also use
17+
# `-Werror`, this warning turns into an error and blocks compilation. Therefore,
18+
# we change this specific diagnostic back to a non-fatal warning.
19+
-Wno-error=variadic-macros
20+
>
21+
)
22+
1123
# Link the test executable with the main library and the test framework/library
1224
target_link_libraries(unittest PRIVATE kaitai_struct_cpp_stl_runtime GTest::GTest GTest::Main)
1325

0 commit comments

Comments
 (0)