Last active
October 13, 2019 02:31
-
-
Save akbyrd/4dd8a2cb32c8a2bac2a4d138d55909b2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Goal: Add type safety and compile time checks to sprintf. | |
// | |
// * Use % as a placeholder for any type, without requiring a type parameter such as %f or %s | |
// * Ensure the number of % placeholders matches the number of parameters at compile time | |
// * Use strcpy where possible, instead of building the string character by character | |
// * Don't do multiple passes over the format to count % placeholders ahead of time | |
// * Not currently worried about custom format specifiers like %.2f (will expand later to allow e.g. {0.2}) | |
// Grammer | |
// % is a placeholder for a value | |
// %! is a '%' literal | |
// %!! is a placeholder for a value followed by a '!' literal | |
// (%% isn't sufficient for a '%' literal because %%% would be ambiguous. Is it <value>% or %<value?) | |
// | |
// (probably wrong because I don't have much practice with BNF) | |
// | |
// <placeholder> ::= "%" | |
// <percent> ::= "%!" | |
// <escape> ::= "!!" | |
// | |
// <string> ::= !<placeholder> [!<placeholder>]... | |
// <format> ::= [<string>] | |
// <format> ::= [<string>] <placeholder><escape> [<format>] | |
// <format> ::= [<string>] <percent> [<format>] | |
// ------------------------------------------------------------------------------------------------- | |
// Utility Functions | |
template <typename Arg0> | |
constexpr auto | |
GetArg(size_t current, size_t target, Arg0&& arg0) | |
{ | |
assert(current == target); | |
return arg0; | |
} | |
template <typename Arg0, typename... Args> | |
constexpr auto | |
GetArg(size_t current, size_t target, Arg0&& arg0, Args&&... args) | |
{ | |
return (current == target) | |
? arg0 | |
: GetArg(current + 1, target, args...); | |
} | |
template <typename... Args> | |
constexpr auto | |
GetArg(size_t target, Args&&... args) | |
{ | |
return GetArg(0, target, args...); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
// Implementation | |
constexpr size_t | |
CountFormatSpecifiers(const char* format) | |
{ | |
size_t count = 0; | |
for (const char* c = format; *c; c++) | |
{ | |
char ahead0 = c[0]; | |
char ahead1 = c[0] ? c[1] : '\0'; | |
char ahead2 = c[0] && c[1] ? c[2] : '\0'; | |
count += (ahead0 == '%' && (ahead1 != '!' || ahead2 == '!')); | |
} | |
return count; | |
} | |
template <typename... Args> | |
inline size_t | |
ToStringImpl(char* buffer, size_t bufferLen, const char* format, size_t formatLen, Args&&... args) | |
{ | |
size_t written = 0; | |
size_t iArg = 0; | |
size_t iBuf = 0; | |
size_t iFmt = 0; | |
auto Step = [&](size_t bufCount, size_t fmtCount) { | |
written += bufCount; | |
iBuf += bufCount * !!buffer; | |
iFmt += fmtCount; | |
}; | |
while (iFmt < formatLen) | |
{ | |
if (format[iFmt] != '%') | |
{ | |
size_t len; | |
for (len = 1; iFmt + len < formatLen; len++) | |
if (format[iFmt + len] == '%') break; | |
if (buffer) strncpy(buffer + iBuf, format + iFmt, len); | |
Step(len, len); | |
} | |
else | |
{ | |
char ahead1 = formatLen - iFmt >= 1 ? format[iFmt + 1] : '\0'; | |
char ahead2 = formatLen - iFmt >= 2 ? format[iFmt + 2] : '\0'; | |
if (ahead1 != '!' || ahead2 == '!') | |
{ | |
size_t len = ToString(buffer + iBuf, bufferLen - iBuf, GetArg(iArg++, args...)); | |
Step(len, 1); | |
} | |
else if (ahead1 == '!' && ahead2 != '!') | |
{ | |
if (buffer) buffer[iBuf] = '%'; | |
Step(1, 2); | |
} | |
else if (ahead1 == '!' && ahead2 == '!') | |
{ | |
if (buffer) buffer[iBuf] = '!'; | |
Step(1, 2); | |
} | |
} | |
} | |
return written; | |
} | |
// TODO: Ensure this works properly when the format is not null terminated (doesn't truncate a character) | |
template <size_t N, typename... Args> | |
char* | |
ToString(const char(&format)[N], Args&&... args) | |
{ | |
assert(CountFormatSpecifiers(format) == sizeof...(args)); | |
size_t bufferLen = ToStringImpl(nullptr, 0, format, N, args...); | |
char* buffer = new char[bufferLen + 1]; | |
size_t len = ToStringImpl(buffer, bufferLen, format, N, args...); | |
buffer[bufferLen] = '\0'; | |
assert(len == bufferLen); | |
return buffer; | |
} | |
// ------------------------------------------------------------------------------------------------- | |
// "User defined" ToString for specific types | |
size_t | |
ToString(char* buffer, size_t bufferLen, int value) | |
{ | |
return snprintf(buffer, bufferLen, "%i", value); | |
} | |
// ------------------------------------------------------------------------------------------------- | |
// Tests | |
#define TO_STRING(format, ...) \ | |
ToString(format, __VA_ARGS__); \ | |
static_assert(CountFormatSpecifiers(format) == CountArgs(__VA_ARGS__)); | |
int main() | |
{ | |
static_assert(CountFormatSpecifiers("") == 0); | |
static_assert(CountFormatSpecifiers("% %!") == 1); | |
static_assert(CountFormatSpecifiers("%! %") == 1); | |
static_assert(CountFormatSpecifiers("% %") == 2); | |
static_assert(CountFormatSpecifiers("x") == 0); | |
static_assert(CountFormatSpecifiers("x% %!") == 1); | |
static_assert(CountFormatSpecifiers("x%! %") == 1); | |
static_assert(CountFormatSpecifiers("x% %") == 2); | |
static_assert(CountFormatSpecifiers("% x %!") == 1); | |
static_assert(CountFormatSpecifiers("%! x %") == 1); | |
static_assert(CountFormatSpecifiers("% x %") == 2); | |
static_assert(CountFormatSpecifiers("% %!x") == 1); | |
static_assert(CountFormatSpecifiers("%! %x") == 1); | |
static_assert(CountFormatSpecifiers("% %x") == 2); | |
#define TEST(res, fmt, ...) \ | |
{ \ | |
char* str = TO_STRING(fmt, __VA_ARGS__); \ | |
assert(strcmp(str, res) == 0); \ | |
} | |
TEST("Foo: 42%", "Foo: %%!", 42); | |
TEST("", "" ); | |
TEST("42%", "%%!", 42); | |
TEST("%42", "%!%", 42); | |
TEST("4242", "%%", 42, 42); | |
TEST("x", "x" ); | |
TEST("x42%", "x%%!", 42); | |
TEST("x%42", "x%!%", 42); | |
TEST("x4242", "x%%", 42, 42); | |
TEST("42x%", "%x%!", 42); | |
TEST("%x42", "%!x%", 42); | |
TEST("42x42", "%x%", 42, 42); | |
TEST("42%x", "%%!x", 42); | |
TEST("%42x", "%!%x", 42); | |
TEST("4242x", "%%x", 42, 42); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment