Support text formatting of common types
A textual representation of a type is very handy when displaying to the console or a GUI. The former use case is the backbone of printf-style debugging which many programmers love doing, myself included. The latter use case is something I use with both sf::Text and ImGui to draw text on screen for various reasons. SFML ought to support formatting types to better support these common use cases.
I propose 2 options but am open to alternatives.
Option 1: Overload std::ostream& operator<<(std::ostream&, const sf::T&)
This is a conventional option and it's already what we're doing in the test/ directory. It's fairly testable. However, this does impose ostream-style formatting on everyone which can get annoying if you're not already using ostream-based logging. Testing these operators requires mutable local variables which makes it difficult to write many tests within the same scope.
Option 2: Overload std::ostream& operator<<(std::ostream&, const sf::T&) and implement std::string sf::toString(const sf::T&)
This option is inclusive of the previous but adds a pure function named sf::toString which offers the same functionality with different ergonomics. I'm thinking that we would implement operator<< in terms of sf::toString so that we don't have to worry about mutating the state of std::cout which can affect other functions using that global.
std::string toString(const sf::T& t)
{
std::ostringstream oss;
oss << std::setprecision(5) << std::fixed << std::hex; // Won't pollute state of std::cout
oss < t.getValue();
return oss.str();
}
std::ostream& operator<<(std::ostream& out, const sf::T& t)
{
return out << sf::toString(t);
}
Either option would let us remove tons of test utilities code which would make me very happy. The types that would initially support text formatting would be those for whom we've already written operator<< overloads but the list may certainly expand over time once we establish a precedent. The same PR that adds these would also add unit tests since testing either option is fairly straightforward.
Overall I'm of the opinion that it's not the class/entity's responsibility to define a (default) string serialization.
C# implements this for all objects with ToString() and while it can be sometimes useful, most classes don't actually override it and even if it is implemented, you usually want other data or have it formatted differently.
The two use-cases you've mentioned here are:
- Debugging output
- Usage in tests
If SFML was a game engine (i.e. a tool for developers), I might be enough of an argument, but on it's own it doesn't really seem to justify extending every and/or a selection of classes
I disagree with eXpl0it3r here and think a default string representation can be very useful -- just not for every class. For example, sf::Vector3 and sf::Color should have one, but sf::Sprite should not.
This is a very common pattern across many languages:
- Java has also
toString(), but it's of limited utility because the default is simply class name + hash. - Kotlin does it a bit better with
data class, wheretoString()is simply listing the value of each field (something likesf::Vector3would be a data class,sf::Spritewould not). - Rust has
DisplayandDebugtraits (traits are interfaces outside a class, comparable to C++20 concepts):-
Displayis a "nice" representation, for embedding things into string. It has to be done manually. Example:(1, 2, 3)forVector3. -
Debugis a more verbose implementation, but can be generated. Example:Vector3 { x: 1, y: 2, z: 3 }.
-
- Python has
__str__. - Lua has the
__tostringmetamethod. - JavaScript has the
prototype.toStringmethod - Godot's GDScript has
_to_string(). - ...
This shows is that it's really C++ which is the odd one here. Not at least because there's no one standard string/stream which is broadly useful.
Back in Thor, I implemented extensions std::string ToString(const T& value).
But I think std::string is not the best type.
Implementing both sf::String toString() and std::ostream& operator<< would be quite useful in my opinion, at least for some core types. Unfortunately it doesn't look like we can generically implement << using sf::toString(), at least not without concepts.
To be clear, nobody is advocating for every class having a string representation nor even most classes having it. We just need a handful of types to support it. Currently the list of types we convert to text are as follows:
-
sf::VideoMode -
sf::Angle -
sf::String -
sf::Time -
sf::Vector2<T> -
sf::Vector3<T> -
sf::BlendMode -
sf::Color -
sf::Transform -
sf::Rect<T>
We may add a few more types to the list but I don't see it growing much further. The only types defining operator== which don't already have string conversions are sf::Packet and sf::IpAddress and that's just because we have no unit tests for those yet.
To me the "lots of languages do so" isn't really convincing. If C++ had such a built-in functionality that requires overriding, then I don't really see anything against that, but why should a library implement this pattern on its own, just because other languages have it?
With fmt/<format> there are way better tools to output and format object information and should really become the defacto standard. I've not once implemented ToString() in any of my C# coding, nor have I missed it on other objects, because, as I said, in the end you want to pick the relevant information yourself.
Not applying it to every class is also something, that puts all of it into question. Why should only some classes have ToString() methods. Who decides when a class becomes worthy and when unworthy? Who says, I'm not interested in all the parameters sent on my SoundBuffer object? Why is debug stringing a Vector2<T> more "acceptable" than a SoundBuffer? It goes against the core principle of consistency.
Still looking for additional use-cases other than debug output and default string serialization (for e.g. tests).
With fmt/
<format>there are way better tools to output and format object information and should really become the defacto standard.
Good point, but we wanted to base SFML 3 on C++17, so std::format won't be accessible for us. It may take another 5 or 10 years until we upgrade the underlying C++ standard again.
I've not once implemented
ToString()in any of my C# coding, nor have I missed it on other objects, because, as I said, in the end you want to pick the relevant information yourself.
I've made a different experience here. In Java and Kotlin I often implement toString() (or use the compiler-generated one). In Rust I derive Debug (auto-generated) for almost all "POD style" structs. Mostly for logging, debugging or visualizing tools. In SFML, printing a position in a sf::Text would be a simple development info.
toString() doesn't prevent you from picking the information yourself. But it provides a reasonable default. Almost everyone prints vectors as (x, y, z), why not provide this?
To me the "lots of languages do so" isn't really convincing. If C++ had such a built-in functionality that requires overriding, then I don't really see anything against that, but why should a library implement this pattern on its own, just because other languages have it?
operator<< is somewhat the convention that C++ uses.
The C++ standards commitee doesn't really care how the language is used -- unlike in any other language, there is no official documentation, not even an API reference, let alone best practices. So everything comes from third-party sources, and best practices are what the community agrees on.
The lack of official guidance is unfortunate, but for me it's not an indicator that something isn't useful. Otherwise, in that logic we wouldn't have needed filesystem interaction before 2017. Every language having toString in some form simply means that the majority of the software industry agreed that this feature adds value.
That doesn't mean I want SFML to become the polyfill library for missing C++ features and provide all kinds of general-purpose tooling. String representation is very limited in scope though, and it's not invasive or forced upon the user (like e.g. sf::Span would be).
Not applying it to every class is also something, that puts all of it into question. Why should only some classes have
ToString()methods. Who decides when a class becomes worthy and when unworthy? Who says, I'm not interested in all the parameters sent on my SoundBuffer object? Why is debug stringing a Vector2 more "acceptable" than a SoundBuffer? It goes against the core principle of consistency.
toString is an operation like operator==. Not all operations are defined on all types.
You can replace toString with operator== in your paragraph and have the exact same issue. It's not really an issue, it might even be a good start to define toString() for those types which are equality-comparable.
I'm partially OK with a separate optional header that provides a set of free function overloads to stringify common types, but absolutely against adding a toString member function to existing types.
I don't think anyone is advocating for adding member functions.
Why in a separate header though? More compile time concerns?
Why in a separate header though? More compile time concerns?
Yes, a toString function would mean at least pulling in std::string or even something from <iostream>.
Would these separate formatting headers still be included in the module-wide headers like <SFML/Graphics.hpp>? If so then the compile time impacts should be the same for many users who aren't using granular headers for specific classes.
How exactly do you propose we break this apart? Do we have one header per module full of formatting functions? One header per class that gets formatting support?
In general I disagree with the mentality that we need to care about compile times to this degree. Have we gotten complaints from users about compile times? Is there a specific compilation time target that we're trying to hit? What use cases are most representative when measuring compile time? Clean builds? Incremental builds? Incremental builds of only specific headers? Are we doing this for the sake of the maintainers and contributors? To save CI time? We spend a non-trivial amount of time and effort optimizing for compile time and I just want to know why the team thinks that effort is worth it because I think our time is better spent elsewhere.
Would these separate formatting headers still be included in the module-wide headers like
<SFML/Graphics.hpp>? If so then the compile time impacts should be the same for many users who aren't using granular headers for specific classes.
I think they should be included for completeness. If <string>/<sstream> is already included in another header, it will not even matter.
Anyway, #include <SFML/Graphics.hpp> is a statement "I can't be bothered about hand-picking headers and will gladly accept worse compile times". If people go that path, let's not make their life harder.
Important is that we provide a way for people who do care. After all, we never know what SFML is used for. It may be an extension library or a bigger project which is already slow to compile. It may be an obscure configuration which has naming conflicts with symbols in <string>. And so on... Back in the day, I absolutely hated it that the Ogre3D engine offered no reasonable way of splitting headers and a simple "Hello World" took almost a minute to compile. Fortunately, we're nowhere near as bad yet 🙂
Of course, there's always a limit. I wouldn't go as far as implementing PImpl everywhere to squeeze out the last second. But when it's relatively easy to achieve, and may even benefit separation of concerns or code organization (like having string serialization code close to each other), I think the improvement is worth it.
It's not my ideal solution but I can get behind putting string conversions in separate headers so long as those headers make it into the module-wide headers so that un-picky users never have to think twice about where they come from. I'm leaning towards having <SFML/<Module>/Formatting.[h|c]pp> pairs where we shove all the formatting code since having like 8 extra graphics headers (and .cpp files) for formatting seems excessively tedious and too granular.
If we agree on the basics then I'm happy to throw up a prototype implementation so we can see how all the details shake out. Once all of that is established, we can do the super fun bikeshedding of determining what formatting style we like.
Does the team have any more thoughts on my proposal for per-module formatting headers? At least enough support to be willing to review a PR?
Yes, I would support one ToString/Format header/impl pair per module 🙂
I unfortunately still can't get behind such an API. It doesn't fulfil any actual use-case and will always remain just an utility. What we lose on the other side is API consistency and we muddy up the API.
I also see the "danger" of people using these functions to actually serialize the classes. Which can lead to questions of expanding it to all classes and forces us to make the output format as part of the API, i.e. no breaking changes allowed.
Personally, I don't see the massive usefulness for debugging, as an actual debugger is way more useful. Can potentially see some use in logging (the only time I indirectly use ToString in C#), but not enough to justify such an addition in my opinion.
I'd like to bump this issue. It would be really helpful to users if we had sf::toString overloads for types that commonly get printed. Providing nothing is overly hostile to users who just want basic utilities to save themselves some hassle. We're not forced to stick to precisely the same string formatting decisions for the lifetime of SFML 3 since these overloads are just debugging/testing utilities after all. I see little downside while adding meaningful convenience for users.
toStringis an operation likeoperator==. Not all operations are defined on all types.
The comparison operator does however serve a proper purpose and is defined when the comparison "makes sense" (and is not overly expensive, e.g. comparing to SoundBuffers byte by byte). I find it much harder to draw such a "makes sense" line for a toString operator, again, why shouldn't e.g. a SoundBuffer not have a string output with sample count, sample frequency, duration and channel count?
Every language having toString in some form simply means that the majority of the software industry agreed that this feature adds value.
"Others do it" has never really been a accepted argument, when adding features to SFML. The claim of "adds value" needs a bit more than simply the existence of the feature. Languages often copy from each other, do they only do it for features, that really add value? How does the value weight against the added complications (support requests, edge cases, etc)?
Providing nothing is overly hostile to users who just want basic utilities to save themselves some hassle.
That's a bit of a hyperbole here. Just because a library doesn't provide stringify options, doesn't make it hostile.
I'm still of the strong opinion that this is not a good fit for SFML:
- It's not a library's responsibility to provide debug utilities as part of the public API, especially in a language that doesn't support/encourage this out of the box.
- We would muddy the public API with pure debug utilities.
- There are no clear criteria why some types should or shouldn't get text formatting support.
- This is basic code that anyone can add to their project as needed, even better customize it to their needs (especially with
<format>on the horizon) - it could even be provided as simple extension to SFML. - We'd introduce part of a public API which output can change between minor releases. Even if we declare the output as not part of the API, we'll run into breaking code, as people will start to rely on it regardless of our statement.
In light of that, I'll close this issue. If you think this is unjustified and a must-have, feel free to re-open it.