Skip to content

Instantly share code, notes, and snippets.

@akbyrd
Last active October 13, 2019 02:31
Show Gist options
  • Save akbyrd/4dd8a2cb32c8a2bac2a4d138d55909b2 to your computer and use it in GitHub Desktop.
Save akbyrd/4dd8a2cb32c8a2bac2a4d138d55909b2 to your computer and use it in GitHub Desktop.
// 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