C++ FString
A quick C++ formatting library
I was working on a logging library, and needed some way to format strings quickly, simple and clean, so I wrote this library.
This library is not the best in general, but for my project needs it is the fastest and the best
Features
- Easy include and just call
fstring()
- Fast Just a simple code to extract and modify the string
- Light No lots of code, just one class with only one function to call
- CrossPlatform It doesn't use any third party libraries, so it should work anywhere
Syntax
// parameters
fstring("{}, {}", "Hello","World"); // "Hello, World"
// padding
fstring("%5Hello%5World%5"); // " Hello World "
// smart padding, divide string to sections, final padding = padding - length of section
fstring("%.10Hello.%%.10World.%"); // "Hello World "
// result
fstring(..).get(); // return to &string
Target
Providing an easy, fast and clean way to format strings
Providing the best performance for millions of times use
Test Info
- OS: Ubuntu 22.04 LTS / 64-bit
- Processor: Intel® CoreTM i7-6500U CPU @ 2.50GHz ×ばつ 4
- Compiler: g++ 12
- Flags: -O3
- C++ Version: 23
Code (GitHub)
/**
* @file fstring.hpp
* @brief A quick C++ formatting library
* @version 0.1
*
* Copyright (c) 2022 Maysara Elshewehy (xeerx.com) ([email protected])
*
* Distributed under the MIT License (MIT)
*/
#pragma once
#include <cstddef> // std::size_t
#include <string> // std::string
#include <stdexcept> // std::runtime_error
#include <ctype.h> // isdigit
#include <vector> // std::vector
#include <algorithm> // std::find
// to save padding begin,end positions and value
typedef struct padd_vec
{
std::size_t begin, end, value;
padd_vec(std::size_t b,std::size_t e, std::size_t v) : begin{b}, end{e}, value{v} {}
} padd_vec;
class fstring
{
private:
std::string src;
std::size_t lpos = 0; // last postion for variables() function
std::vector<padd_vec> poss; // to remember positions before formatting in padding()
protected:
// find paddings in string and handle it
void padding() noexcept(false)
{
// init
std::size_t pos = 0, end = 0;
// handle
while(pos < src.length())
{
// find '%'
if((pos = src.find('%',(pos == 0) ? 0 : pos+1)) == std::string::npos) break;
// now we got the position of first "%", so increase position to get next character
std::size_t tmp = pos; // save begin position
pos ++;
// what if the character "%" is last character in the string ?
if(pos >= src.length()) break;
// find '.' : for smart padding (padding - length of section)
bool smart_padding = src.at(pos) == '.' ? true : false;
if(smart_padding)
{
// increase position to get the next character after "%."
pos ++;
// what if the character '.' is last character in the string ?
if(pos >= src.length()) break;
}
// find padding
ushort padd = 0; // to store padding value
ushort digits = 0; // to store digits count
while(pos < src.length())
{
// check if digit is number
if(!isdigit(src.at(pos))) break;
// get the number
ushort n = src.at(pos)-'0';
// if the first number is zero
if(!n && !padd) throw std::runtime_error("Padding can not be zero OR start with zero");
// calc padding
padd *= 10; padd += n;
// increase position to get the next character
pos ++;
// increase count of digits
digits ++;
}
// if padding is zero so there is nothing to do so skip it !
if(!padd) continue;
// smart padding: save positions to apply after putting the variables value
if(smart_padding)
{
// find ".%" : the end of section
if ((end = src.find(".%", pos)) == std::string::npos) throw std::runtime_error("smart padding must start by %. and ended by .%");
poss.push_back(padd_vec(tmp,end-4,padd));
src.erase(end,2); // erase ".%"
src.erase(tmp,digits+2); // erase "%.n"
}
// normal padding: apply
else
{
// erase %n
src.erase(tmp,digits+1);
// add spaces(padding)
src.insert(tmp , std::string(padd, ' '));
// update position to avoid searching in spaces area
pos += padd - (digits+1);
}
}
}
// apply smart padding
void apply ()
{
for (auto i = poss.begin(); i < poss.end(); i++)
{
// get length of section
std::size_t length = i->end - i->begin;
// if length < padding value so there are a space to add padding
if(i->value > length)
{
// calc padding value: padding - length = the rest of space
std::size_t padd = i->value - length;
// add spaces(padding)
src.insert(i->end, std::string(padd, ' '));
// now we updated the string value, so the length is updated
// so we need to update the upcoming positions with: +=padd
for (auto t = i; t < poss.end(); t++) { t->begin += padd; t->end += padd; }
}
}
}
// inline helpers for variables()
inline std::string tostr(std::string ref) { return ref; }
inline std::string tostr(const char * ref) { return std::string(ref); }
template<typename Arg>
inline std::string tostr(Arg ref) { return std::to_string(ref); }
// replace "{}" by values
template<typename T>
void variables(T arg)
{
// to avoid error(out of range exception)
if(lpos == std::string::npos) return;
// find "{}"
if ((lpos = src.find("{}", lpos)) == std::string::npos) return;
// store arg value as string
std::string val = tostr(arg);
// save
src.erase(lpos,2); // erase "{}"
src.insert(lpos,val); // add arg value in position of "{}"
// now we updated the string value, so the length is updated
// so we need to update the upcoming positions with: +=val.length()
for (auto i = poss.begin(); i < poss.end(); i++)
{
// skip values smaller than the lpos
if(i->end <= lpos) continue;
// increase
i->end += val.length();
// we cleared the "{}" characters so we need to go back 2 characters if possible
if(i->end > 2) i->end -=2;
// begin of non-first
if(i != poss.begin())
{
i->begin += val.length();
if(i->begin > 2) i->begin -=2;
}
}
}
public:
// constructor
template<typename... T>
fstring(std::string _src, T&& ...args) : src{_src}
{
// performance improvement, see: https://cplusplus.com/reference/vector/vector/reserve/
poss.reserve( 5);
src .reserve(250);
padding();
( variables(args) , ...);
apply ();
}
// get result
std::string& get(){ return src; }
};
Questions
- Is there anything that could be improved?
- Are there errors?
- Do you have any tips or comments?
Benchmark
Benchmark | Time | CPU | Iterations |
---|---|---|---|
fstring1 | 164 ns | 96.5 ns | 7226964 |
fmt_format | 1136 ns | 526 ns | 1641526 |
Updates / Edits
i will add edits or updates here
- Fix some spelling errors
- benchmark
- update benchmark
- Improve performance, see github
- update benchmark, thanks @G. Sliepen
2 Answers 2
This library is not the best in general, but for my project needs it is the fastest and the best
Did you run any benchmarks? How does it compare against iostream, printf()
and std::format()
for your use cases? Without any numbers to back it up, a claim like "the fastest" is meaningless.
Easy include and just call
fstring()
But fstring()
by itself doesn't do anything. You have to call get()
as well to get the formatted string out. Why not make fstring()
a function that returns a std::string
?
CrossPlatform It doesn't use any third party libraries, so it should work anywhere
Sure, but it makes use of the standard library, and since C++20 we have std::format()
, but before that you could also do padding using std::setw()
. And then you don't need your library either, which itself is a third-party library in the eyes of other people.
C++ Version: 23
C++23 is not even released yet. Maybe you mean that your code will work with C++23 when that is released? But it is much more useful to instead report what the minimum required C++ version is that your code will work with. Which is C++17, since you use fold expressions.
Is there anything that could be improved?
This library might be useful for the use case you have in mind, but it is very simple and lacks most of the features people expect from a formatting library.
It is also weird to see both {}
and %
being used in the format string. I would pick one of them and use it for padding and string replacement.
The only arguments that are supported are strings or things for which std::to_string()
works. But iostreams and std::format()
support formatting everything which has an operator<<(std::ostream&)
overload.
What if you want to print a literal {}
or %
? What if you want to smart-pad something with a .
in it?
Are there errors?
There is a lack of errors. What if there are more or less {}
s than there are arguments passed to the constructor? It would be nice if those conditions would cause exceptions to be thrown as well.
Do you have any tips or comments?
There are other things that can be improved. foo ? true : false
is redundant, and can simply be replaced by foo
.
The constructor takes arguments by r-value references, but then proceeds to pass them by value to variables()
. Either pass them by const
reference, or by r-value
references all the way (and using std::forward<>()
to pass them to other functions).
Benchmark your code against iostreams, printf()
and std::format()
.
DSL design
Think about the DSL you want to implement, write its rules down, and then try to find the corner-cases.
What should happen if the string in the smart-padding is bigger than the requested padding? I guess just overflow, but snipping is also a valid (and sometimes needed) choice. Sometimes, only snipping might be wanted.
What if I want to dynamically give the size?
Left-justifying is nice. What about centering and right-justifying though?
What will you do if one
char
does not correspond exactly to one character? Just ignore that?Be prepared for someone wanting to print your special characters too. So allow to escape them somehow.
In the end, compare to
<format>
,<iostream>
,<cstdio>
, and whatever other method you have to work with.
Is it simpler, faster, more portable, smaller, more convenient?
Measure and ponder.
Code design
You really don't have to make everything a class in C++.
In your specific case, a simple function template returning astd::string
would be the best choice.Avoid copying
std::string
s. Doing so can be quite costly.In line with the above, avoid inserting or erasing in the middle of a string. Plan how you want to construct your result, and then build it in one go.
Allow inserting things which you can only feed to a
std::ostream
, not only those supportingstd::tostring()
.
variables()
to each argument. \$\endgroup\$std::format()
returns astd::string
, so to make it fair you should callget()
on yourfstring
. The format strings also have slight differences. You only tested one particular string, you didn't test padding. \$\endgroup\$%5
doesn't do anything forstd::format()
, you should use something else to have it pad. \$\endgroup\$