Skip to content

Commit

Permalink
Utility: make format() work with String and StringView outputs.
Browse files Browse the repository at this point in the history
Now that String is implicitly convertible to Array<char>, it shouldn't
cause any backwards-incompatible issues in existing code.

The case with formatInto() and char[] was an interesting one, hopefully
the overload is enough to prevent fires and explosions.
  • Loading branch information
mosra committed Feb 19, 2022
1 parent aa16e7f commit ea9f217
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 236 deletions.
5 changes: 5 additions & 0 deletions doc/corrade-changelog.dox
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,11 @@ namespace Corrade {
code that passes @cpp const char* @ce to it. Given that either type is
valid now, the code has to be updated to explicitly pass that type to the
function.
- @ref Utility::format() now returns a @ref Containers::String instead of
@ref Containers::Array and @ref Utility::formatInto() now takes a
@ref Containers::MutableStringView instead of @ref Containers::ArrayView.
The types are however implicitly convertible and thus breakages are not
expected in majority of existing code.

@subsection corrade-changelog-latest-documentation Documentation

Expand Down
24 changes: 12 additions & 12 deletions doc/snippets/Utility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -660,38 +660,38 @@ static_cast<void>(b);
#endif

{
/* [formatString] */
std::string s = Utility::formatString("{} version {}.{}.{}, {} MB",
/* [format] */
Containers::String s = Utility::format("{} version {}.{}.{}, {} MB",
"vulkan.hpp", 1, 1, 76, 1.79);
// vulkan.hpp version 1.1.76, 1.79 MB
/* [formatString] */
/* [format] */
static_cast<void>(s);
}

{
/* [formatString-numbered] */
std::string s = Utility::formatString("<{0}><{1}>Good {}, {}!</{1}></{0}>",
/* [format-numbered] */
Containers::String s = Utility::format("<{0}><{1}>Good {}, {}!</{1}></{0}>",
"p", "strong", "afternoon", "ma'am!");
// <p><strong>Good afternoon, ma'am!</strong></p>
/* [formatString-numbered] */
/* [format-numbered] */
static_cast<void>(s);
}

{
/* [formatString-escape] */
std::string s = Utility::formatString("union {{ {} a; char data[{}]; }} caster;",
/* [format-escape] */
Containers::String s = Utility::format("union {{ {} a; char data[{}]; }} caster;",
"float", sizeof(float));
// union { float a; char data[4]; } caster;
/* [formatString-escape] */
/* [format-escape] */
static_cast<void>(s);
}

{
/* [formatString-type-precision] */
std::string s = Utility::formatString("path {{ fill: #{:.6x}; stroke: #{:.6x}; }}",
/* [format-type-precision] */
Containers::String s = Utility::format("path {{ fill: #{:.6x}; stroke: #{:.6x}; }}",
0x33ff00, 0x00aa55);
// path { fill: #33ff00; stroke: #00aa55; }
/* [formatString-type-precision] */
/* [format-type-precision] */
}

{
Expand Down
9 changes: 6 additions & 3 deletions src/Corrade/Utility/Debug.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ by this option either), build Corrade with the option disabled.
@see @ref Warning, @ref Error, @ref Fatal, @ref CORRADE_ASSERT(),
@ref CORRADE_INTERNAL_ASSERT(), @ref CORRADE_INTERNAL_ASSERT_OUTPUT(),
@ref AndroidLogStreamBuffer, @ref formatString()
@ref AndroidLogStreamBuffer, @ref format(), @relativeref{Utility,print()}
@todo Output to more ostreams at once
*/
class CORRADE_UTILITY_EXPORT Debug {
Expand Down Expand Up @@ -917,6 +917,7 @@ template<class T, class U> Debug& operator<<(Debug& debug, const std::pair<T, U>
Same as @ref Debug, but by default writes output to standard error output.
Thus it is possible to separate / mute @ref Debug, @ref Warning and @ref Error
outputs.
@see @ref printError()
*/
class CORRADE_UTILITY_EXPORT Warning: public Debug {
public:
Expand Down Expand Up @@ -998,8 +999,10 @@ class CORRADE_UTILITY_EXPORT Warning: public Debug {
/**
@brief Error output handler
@copydetails Warning
@see @ref Fatal
Same as @ref Debug, but by default writes output to standard error output.
Thus it is possible to separate / mute @ref Debug, @ref Warning and @ref Error
outputs.
@see @ref Fatal, @ref printError()
*/
class CORRADE_UTILITY_EXPORT Error: public Debug {
friend Fatal;
Expand Down
10 changes: 5 additions & 5 deletions src/Corrade/Utility/Format.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ void Formatter<std::string>::format(std::FILE* const file, const std::string& va

namespace {

int parseNumber(Containers::StringView format, std::size_t& formatOffset) {
int parseNumber(const Containers::StringView format, std::size_t& formatOffset) {
int number = -1;
while(formatOffset < format.size() && format[formatOffset] >= '0' && format[formatOffset] <= '9') {
if(number == -1) number = 0;
Expand Down Expand Up @@ -413,21 +413,21 @@ template<class Writer, class FormattedWriter, class Formatter> void formatWith(c

}

std::size_t formatInto(const Containers::ArrayView<char>& buffer, const char* const format, BufferFormatter* const formatters, std::size_t formatterCount) {
std::size_t formatInto(const Containers::MutableStringView& buffer, const char* const format, BufferFormatter* const formatters, std::size_t formatterCount) {
std::size_t bufferOffset = 0;
formatWith([&buffer, &bufferOffset](Containers::ArrayView<const char> data) {
if(buffer) {
if(buffer.data()) {
CORRADE_ASSERT(data.size() <= buffer.size(),
"Utility::formatInto(): buffer too small, expected at least" << bufferOffset + data.size() << "but got" << bufferOffset + buffer.size(), );
/* strncpy() would stop on \0 characters */
/* data.size() can't be 0 because that would make the above assert
fail, thus data can't be nullptr either and so we don't need to
check anything to avoid calling memcpy() with a null pointer */
std::memcpy(buffer + bufferOffset, data, data.size());
std::memcpy(buffer.data() + bufferOffset, data, data.size());
}
bufferOffset += data.size();
}, [&buffer, &bufferOffset](BufferFormatter& formatter, int precision, FormatType type) {
if(buffer) {
if(buffer.data()) {
formatter.size = formatter(buffer.suffix(bufferOffset), precision, type);
CORRADE_ASSERT(bufferOffset + formatter.size <= buffer.size(),
"Utility::formatInto(): buffer too small, expected at least" << bufferOffset + formatter.size << "but got" << buffer.size(), );
Expand Down
59 changes: 34 additions & 25 deletions src/Corrade/Utility/Format.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Provides type-safe formatting of arbitrary types into a template string,
similar in syntax to Python's [format()](https://docs.python.org/3.4/library/string.html#format-string-syntax).
Example usage:
@snippet Utility.cpp formatString
@snippet Utility.cpp format
# Templating language
Expand All @@ -60,15 +60,15 @@ ordering (as shown above), or be numbered, such as `{2}`. Zero means first item
from @p args, it's allowed to repeat the numbers. An implicit placeholder
following a numbered one will get next position after. Example:
@snippet Utility.cpp formatString-numbered
@snippet Utility.cpp format-numbered
Unlike in Python, it's allowed to both have more placeholders than arguments or
more arguments than placeholders. Extraneous placeholders are copied to the
output verbatim, extraneous arguments are simply ignored.
In order to write a literal curly brace to the output, simply double it:
@snippet Utility.cpp formatString-escape
@snippet Utility.cpp format-escape
# Data type support
Expand Down Expand Up @@ -133,14 +133,14 @@ Strings, characters (integers with the @cpp 'c' @ce type specifier) | If the str
Example of formating of CSS colors with correct width:
@snippet Utility.cpp formatString-type-precision
@snippet Utility.cpp format-type-precision
# Performance
This function always does exactly one allocation for the output array. See
@ref formatInto(std::string&, std::size_t, const char*, const Args&... args)
for an ability to write into an existing string (with at most one reallocation)
and @ref formatInto(const Containers::ArrayView<char>&, const char*, const Args&... args)
for an ability to write into an existing @ref std::string (with at most one
reallocation) and @ref formatInto(const Containers::MutableStringView&, const char*, const Args&... args)
for a completely zero-allocation alternative. There is also
@ref formatInto(std::FILE*, const char*, const Args&... args) for writing to
files or standard output.
Expand All @@ -159,10 +159,10 @@ serializing text files.
@see @ref formatString(), @ref formatInto(), @ref print(), @ref printError()
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
template<class ...Args> Containers::Array<char> format(const char* format, const Args&... args);
template<class ...Args> Containers::String format(const char* format, const Args&... args);
#else
/* Done this way to avoid including <Containers/Array.h> for the return type */
template<class ...Args, class Array = Containers::Array<char>> Array format(const char* format, const Args&... args);
/* Done this way to avoid including <Containers/String.h> for the return type */
template<class ...Args, class String = Containers::String, class MutableStringView = Containers::MutableStringView> String format(const char* format, const Args&... args);
#endif

/**
Expand All @@ -179,7 +179,15 @@ See @ref format() for more information about usage and templating language.
@experimental
*/
template<class ...Args> std::size_t formatInto(const Containers::ArrayView<char>& buffer, const char* format, const Args&... args);
template<class ...Args> std::size_t formatInto(const Containers::MutableStringView& buffer, const char* format, const Args&... args);

/** @overload */
/* This is needed as otherwise calling formatInto() with char[] as the first
argument would use the MutableStringView constructor that relies on strlen()
to determine the size, which isn't wanted in this case */
template<class ...Args, std::size_t size> std::size_t formatInto(char(&buffer)[size], const char* format, const Args&... args) {
return formatInto(Containers::MutableStringView{buffer, size}, format, args...);
}

/**
@brief Format a string into a file
Expand Down Expand Up @@ -329,29 +337,30 @@ struct FileFormatter {
const void* _value;
};

CORRADE_UTILITY_EXPORT std::size_t formatInto(const Containers::ArrayView<char>& buffer, const char* format, BufferFormatter* formatters, std::size_t formattersCount);
CORRADE_UTILITY_EXPORT std::size_t formatInto(const Containers::MutableStringView& buffer, const char* format, BufferFormatter* formatters, std::size_t formattersCount);
CORRADE_UTILITY_EXPORT void formatInto(std::FILE* file, const char* format, FileFormatter* formatters, std::size_t formattersCount);

}

#ifndef DOXYGEN_GENERATING_OUTPUT
template<class ...Args, class Array> Array format(const char* format, const Args&... args) {
Array array;
/* array is nullptr here, so we get just the size. Can't pass just nullptr,
because that would match the formatInto(std::FILE*) overload :( */
const std::size_t size = formatInto(array, format, args...);
/* printf() always wants to print the null terminator, so allow it, and
then recreate the Array to be of a correct size again. Once we switch
away from printf() this workaround could be removed. The upcoming
Containers::String class will probably have something similar, though
implicit. */
array = Array{NoInit, size + 1};
formatInto(array, format, args...);
return Array{array.release(), size};
template<class ...Args, class String, class MutableStringView> String format(const char* format, const Args&... args) {
/* Get just the size first. Can't pass just nullptr, because that would
match the formatInto(std::FILE*) overload, can't pass a String because
it's guaranteed to always point to a null-terminated char array, even if
it's empty. */
const std::size_t size = formatInto(MutableStringView{}, format, args...);
String string{NoInit, size};
/* The String is created with an extra byte for the null terminator, but
since printf() always wants to print the null terminator, we need to
pass a view *including* the null terminator to it -- which is why we
have to create the view manually. Once we switch away from printf() this
workaround can be removed. */
formatInto(MutableStringView{string.data(), size + 1}, format, args...);
return string;
}
#endif

template<class ...Args> std::size_t formatInto(const Containers::ArrayView<char>& buffer, const char* format, const Args&... args) {
template<class ...Args> std::size_t formatInto(const Containers::MutableStringView& buffer, const char* format, const Args&... args) {
Implementation::BufferFormatter formatters[sizeof...(args) + 1] { Implementation::BufferFormatter{args}..., {} };
return Implementation::formatInto(buffer, format, formatters, sizeof...(args));
}
Expand Down
5 changes: 2 additions & 3 deletions src/Corrade/Utility/FormatStl.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ namespace Corrade { namespace Utility {
@brief Format a string
Same as @ref format(), but returning a @ref std::string instead of
@ref Containers::Array.
@ref Containers::String.
*/
template<class ...Args> std::string formatString(const char* format, const Args&... args);

Expand All @@ -57,8 +57,7 @@ terminating @cpp '\0' @ce character. Example usage:
@snippet Utility.cpp formatInto-string
See @ref formatString() for more information about usage and templating
language.
See @ref format() for more information about usage and templating language.
@experimental
*/
Expand Down
Loading

0 comments on commit ea9f217

Please sign in to comment.