Video Coming Soon...

Created by Zed A. Shaw Updated 2024-09-06 01:36:52

09: Using the fmt Library

I find the C++ style of formatting strings using a cout style operation to be...odd. It works, sure, but it's not how any other programming language does it. Other programming languages use something called a "format string" or possibly a "template" to craft strings from other data. In this exercise we'll use a nice library called fmt to format strings.

The purpose of this exercise is two-fold:

  1. Learn how fmt works, which is also very similar to the C++ std::format with additional features.
  2. Learn how to add other people's open source projects to your own project to get their features.

Starter Code

The first step in using fmt is to #include it, and then call fmt::print(). Here's your usual starter code but with fmt ready to go:

View Source file ex09a.cpp Only

#include <fmt/core.h>
#include <iostream>

using namespace std;

int main() {
  fmt::print("Hello World!\n");
  return 0;
}

Before this will work you need to configure your build to bring in the fmt library's code. Type this up but remember compilation will fail until you add the library to your meson.build.

Adding fmt to Meson

To add fmt to your meson.build file by adding one line after the project() part:

fmt = dependency('fmt')

Next you need to write your executable() line to add fmt as a dependency:

executable('ex09', 'ex09.cpp', dependencies: fmt)

This is the same as your other lines, but only adds dependencies: fmt at the end. When you do this meson will know you want to link (aka combine) your code with the fmt library's code to create your ex09 executable.

The next thing you need to do is get meson to download the fmt library for you:

mkdir subprojects
meson wrap install fmt

After this the subprojects directory will have the code for fmt ready to use on this, and any other executable() lines you write. Just add dependencies: fmt to them and add the library to your code.

Using Static Linking

One small problem with our build is that it's currently configured to use "dynamic linking" but we need static linking. "Dynamic linking" is where your program doesn't actually contain the code for the fmt library, instead it references a .so, .dylib, or .dll file on your computer. Any time you run ex09 it will look in your program, see you are using the libfmt library, and then "link it" in just as it runs.

The problem with dynamic linking is it requires your program knows where all these libraries are located. Normally this isn't a problem since there's a standard location for them, but when you're working on your programs these dynamic libraries are held deep inside the subprojects directory, so your ex09 executable can't find it. The result is you try to run ex09 and either get an error, or on Windows it does nothing.

To test this, try compiling your ex09 executable and run it to see if you get an error. On Windows, try doing it this way:

meson compile -C builddir
start .\builddir\ex09

This is how you "double-click" on ex09 from inside Terminal. Now you should get a pop-up error saying that libfmt.dll is missing. It is stupid that Microsoft doesn't print this message out in the Terminal, but hey, welcome to programming.

Static linking is where your compiler takes the code from the libfmt library, and simply embeds all of the code you use into the your ex09 executable. The advantage of this is whenever you run your ex09 it doesn't have to find the libfmt library anywhere as it's already there. We want static compilation for a lot of reasons, not just because it's easier to run it, but also because it makes debugging easier to do later.

To enable static linking you need to rebuild your builddir with a new configuration. On OSX and Linux you would do this:

rm -rf builddir
meson setup --prefer-static --default-library=static builddir

On Windows it's the exact same meson setup but the command to delete the builddir directory is different:

rm -recurse -force builddir
meson setup --prefer-static --default-library=static builddir

Now recompile your code and you should see this:

meson compile -C builddir
./builddir/ex09
Hello World!

Using fmt In Your Code

With your little Hello World working we can finally explore what the fmt library can do you for you. Here's an updated piece of code that demonstrates all the main features of fmt:

View Source file ex09.cpp Only

#include <fmt/core.h>
#include <iostream>
#include <string>

using namespace std;

int main() {
  int a_int = 1234;
  long a_long = 37812394;
  long long a_long_long = -68354647782938476;
  float a_float = 0.1234;
  double a_double = 3.23499;

  fmt::println("Here's an int {}", a_int);

  string msg = fmt::format(
      "An int {}\n"
      "A long {}\n"
      "A long long {}\n"
      "A float {}\n"
      "A double {}\n",
      a_int, a_long, a_long_long,
      a_float, a_double);

  fmt::println("The result is:");
  fmt::print("{}", msg);

  return 0;
}

As usual, get this to work a little at a time, but the large block in the center is a little tricky. I normally tell you to "type a little at a time" but if you did one line this code wouldn't compile. Let's focus on just that one block:

string msg = fmt::format(
    "An int {}\n"
    "A long {}\n"
    "A long long {}\n"
    "A float {}\n"
    "A double {}\n",
    a_int, a_long, a_long_long,
    a_float, a_double);

This is actually "one line" of code, but obviously it's spread across multiple lines. If I convert this into a pattern it would be look like this:

string msg = fmt::format(<template>, <args>);

The <template> is a string that uses {} to say where format should "inject" one of the variables listed in <args>. The <args> are then a comma separated list of variables you defined earlier. How this becomes multiple lines is with a couple of tricks:

  1. Any time you have two or more strings next to each other they will be merged together. So you write "Hello" "World" it would be merged together into "HelloWorld". Yes, the space in the middle is skipped over and they're just combined together.
  2. This combining of strings is something the compiler does, and it ignores all whitespace, so newlines and tabs are also skipped.
  3. This means you can make big strings formatted nicely in your code by just putting them on multiple lines.
  4. The next part that makes this work across multiple lines is how C++ processes the <args>. I am listing out the variables to use separated by ,. C++ knows I'm not done with the list yet because it hasn't seen the ) that ends the function call to format(). That means when it hits the end of the line it moves to the next line and keeps processing the <args> until it reaches the ) that ends the function call.

Given all that, how are you supposed to type this in a little bit at a time? By only doing one <template> and one <args> at a time. For example, I would write:

string msg = fmt::format("An int {}\n", a_int);

That's one line and makes sure your format call is working. Now we add the next piece and turn it into multiple lines:

string msg = fmt::format(
    "An int {}\n"  // just newline
    "A long {}\n", // <-- move the comma
    a_int, a_long);

See how I just write the next part of the format, but I move the , (comma) to the end of the next line? Once you have this working you can then add each part of the template, and each new argument for it, and complete the code.

Complete Code Breakdown

Once you get the code working we can review what's going on. I'm only going to cover the parts you don't already know from previous exercises:

1
The only new include we have is to include <fmt/core.h> so we can use the fmt library.
7-12
This is very similar to the code from ex08.cpp but we aren't doing the conversions from strings. Instead I just write the numbers as constants directly.
14
This is how you print a variable to the screen. This uses a new function fmt::println() that is similar to fmt::print(), but it adds the \n for you. It takes two arguments, one is a template and the other is a list of variables. the list of variables can be as long as you want, separating each variable by , (commas). Each place you write {} in the template has to match with a variable after the template in order or else you'll get an error.
16
I create a string msg instead of creating an ostringstream as in ex08.cpp. I then assign it to the result of fmg::format( which you should remember is a function call. This one calls the function format in the fmt namespace/library. You should also see that I end the line with ( which means I'm going to use multiple lines to finish this function call.
17-22
These are each one line, and look like one string, but from the previous section you know that these are all merged together to make one giant string. Each one has a {} to place a variable, and a \n to end the line in Terminal.
23-24
This is the list of variables that we match to each {} in the above multi-line format string. Don't be fooled, this large multi-line format string is exactly the same as the one on line 14, it's just formatted so that you can see it as multiple lines in code to match the multiple lines in Terminal.
24
At the end of line 24 I use ); to finish the line of code, and this is matched up to the ( at the end of line 16. If you place your editor's cursor on this ) or the ( it should "match" them up and highlight the other one. If it doesn't then you may have an error in your code.
26
I use another fmt::println() to print a message.
27
Then I print out the msg variable by use a "{}" format and only giving msg as the variable for it. This may cook your brain some but remember that the result of lines 16-24 is to create a string that has been formatted already. All this print is doing is printing out that result. You could change this to use cout << msg; for the same result.

More on Functions

These functions we're using from fmt are special in a couple of ways:

  1. You are getting them not just from the #include <fmt/core.h> at the top, but also by changing your meson.build to add the fmt library. If you've never written code before this is a massive topic that we'll explore more, but think about them as "paid DLC for C++." You've already got the c++ compiler, you're just adding the DLC of fmt so you can get the new content. That content is new features and functions for your programs.
  2. These functions also take variable numbers of arguments, which means you can add as many as you need to match your format templates. This is not normal though since most C++ code only uses functions that take one or two variables on average.

When you use these functions, I want you to think of them as transformers They take in a sequence of variables as arguments and transform them into a new result. Not all functions do this, but generally that's what a function is supposed to do. In this case, the fmt library functions take a template and a list of variables, and transforms them into a formatted string or prints the formatted string to the Terminal.

Break It

As usual you should be trying to break this code to become familiar with the kinds of errors you get from the C++ compiler. C++ is notorious for having humongous convoluted error output so it's a good idea to cause them early. Some of these ideas will cause some insane error outputs:

  1. Give fmt::print too few or too many variables for the format. Which one gave you an error?
  2. Call fmt::print(msg) directly instead of fmt::print("{}", msg). This one's a bad one to read, but it's effectively saying that the first parameter to fmt::print has to be a "constant expression", and you passed in a variable that's not constant. We'll get into that later, but just keep this in mind so you can recognize the error later when you see it.
  3. Misspell something.

Try those then try as many other ways to cause errors. Bonus points if you can get this to crash in some way.

Further Study

  1. The final line fmt::print("{}", msg) is a little weird. It's a style choice to either always use fmt, always use cout, or to mix them. Try changing this line to use cout and see if you like it.
  2. Get rid of the fmt:: on all the function calls. Do you remember what you need to do for that to work?
  3. How does this compare to ex08.cpp? Which way of creating a formatted string do you like better?
  4. Take the code from ex08.cpp and replicate its functionality using fmt from this exercise.
  5. ADVANCED If you can use fmt::format to create strings from numbers, and you can use the stoi style functions to convert strings to numbers, and every function returns a result, then can you get weird and have stoi call fmt::format to double convert a number? Don't do this in real code, but try to figure it out as an exercise.
  6. ADVANCED Meson has a list of wrap libraries in their WrapDB list. Find one library and add it to your meson.build to see if you can. I suggest that if you pick one that doesn't work to remove it and try another library that does work.
Previous Lesson Next Lesson

Register for Learn C++ the Hard Way

Register today for the course and get the all currently available videos and lessons, plus all future modules for no extra charge.