5
\$\begingroup\$

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® Core™ 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) (maysara.elshewehy@gmail.com)
 *
 * 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

  1. Fix some spelling errors
  2. benchmark
  3. update benchmark
  4. Improve performance, see github
  5. update benchmark, thanks @G. Sliepen
\$\endgroup\$
9
  • 1
    \$\begingroup\$ @vnp: it's a fold expression that applies variables() to each argument. \$\endgroup\$
    – G. Sliepen
    Jul 8 at 20:08
  • 1
    \$\begingroup\$ Some issues with your benchmark: std::format() returns a std::string, so to make it fair you should call get() on your fstring. The format strings also have slight differences. You only tested one particular string, you didn't test padding. \$\endgroup\$
    – G. Sliepen
    Jul 9 at 9:34
  • 1
    \$\begingroup\$ Hm, running your benchmark code on an i5-7200U @ 2.50 GHz, I get 159 ns for fstring1 and 65.6 ns for fmt_format. It's also a bit strange that your value for "Time" is much higher than "CPU". Of course, %5 doesn't do anything for std::format(), you should use something else to have it pad. \$\endgroup\$
    – G. Sliepen
    Jul 9 at 13:37
  • 2
    \$\begingroup\$ This review slightly reminds me of codereview.stackexchange.com/questions/275633/… EDIT: Not just slightly. It's even by the same OP. \$\endgroup\$
    – Ray
    Jul 10 at 11:53
  • 1
    \$\begingroup\$ ^That. I'm pretty sure there was one major point from that review being: write tests. And you still didn't. \$\endgroup\$
    – Passer By
    Jul 10 at 12:55

2 Answers 2

9
\$\begingroup\$

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().

\$\endgroup\$
7
\$\begingroup\$

DSL design

Think about the DSL you want to implement, write its rules down, and then try to find the corner-cases.

  1. 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.

  2. What if I want to dynamically give the size?

  3. Left-justifying is nice. What about centering and right-justifying though?

  4. What will you do if one char does not correspond exactly to one character? Just ignore that?

  5. Be prepared for someone wanting to print your special characters too. So allow to escape them somehow.

  6. 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

  1. You really don't have to make everything a class in C++.
    In your specific case, a simple function template returning a std::string would be the best choice.

  2. Avoid copying std::strings. Doing so can be quite costly.

  3. 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.

  4. Allow inserting things which you can only feed to a std::ostream, not only those supporting std::tostring().

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.