top of page
Connecting Dots

Using Modern C++ for a Robust sprintf

Updated: Jul 6

-- Dan Higgins, CEO Tech 52 Studios, LLC


Using Modern C++ to Enable Compile-Time Checks for sprintf and Other Old Favorites


Remember the days when sprintf was our primary way of building strings? Maybe not? Today, most of the time we wrap sprintf behind other APIs, but it's still there and valuable. Think of those other languages that have a string.Format method—in the 'C' family of languages, building a string with replacements is still how we do things.


What happens though, if you call sprintf with an object? Depends on the compiler - but sometimes you get an address, sometimes you get a crash, sometimes it seems to be handled. Unpredictable behavior!


In this article, we discuss how we get the compiler to stop and inform us during builds that we've passed in an invalid argument type to sprintf. This doesn't do run time evaluation like fmt does, but it will tell us, for example, we passed in an std::string instead of calling .c_str() on it.


Jump right to the code and test it out here:


Below shows an example of how this prevents run time errors by catching them at compile time.


Notice, sprintf can be a bit of a troublemaker if you're not careful. One wrong type passed in, and boom—you're dealing with runtime errors that are often a pain to debug. Fortunately, modern C++ provides us with the tools to enforce type safety at compile time, making sure that our old friend sprintf behaves nicely.



Why Compile-Time Checks?


The main issue with sprintf is that it doesn't check types at compile time. This means that you might pass an incompatible type and only find out at runtime, leading to crashes or subtle bugs. Modern C++ (C++20+) introduces features like concepts and constexpr functions that allow us to perform these checks at compile time, catching errors early in the development process before we find a way to run into them during runtime.

Implementation

Here's a look at how we can use modern C++ features to enforce compile-time checks for sprintf:


Define a Concept for Safe Types

First, we define a concept to determine if a type is safe for sprintf:

What are Concepts in C++ 20?
  • Introduction to Concepts: Concepts are a very powerful new feature in C++20 which allows you to specify constraints on template parameters. They make template code more readable and help catch errors at compile time. Think of it as a rule the compiler checks against for types that you get to code and customize, the use in many places such as templates, and constexpr expressions.

  • Usage of Concepts: Concepts are used to ensure that types passed to templates meet specific criteria, improving code reliability and maintainability.


Below is an example concept we'll use, which ensures that the argument is an int, float, or convertible to a const char*. Using this will ensure a no one passed in, for example, an object like std::string.



Now, we add in some internal helpers which will check each argument passed in, and use the compile time concept to force a compile time error is someone passes in something like an std::string instead of calling .c_str().



Next, we need to call it, but unfolding the var args and processing each argument individually.




Finally usage example:




See all the code and test it out here:


Conclusion

By leveraging modern C++ features, we can enforce compile-time safety checks, significantly reducing the risk of runtime errors associated with sprintf and similar functions. This approach not only makes our code safer but also helps in catching potential issues early in the development cycle.


Using these techniques, developers can continue to utilize the power and flexibility of sprintf while ensuring that their code adheres to modern safety standards. This balance between old and new C++ paradigms allows for robust and maintainable code.


Happy coding!


-- Dan Higgins

CEO, Tech 52 Studios, LLC


12 views0 comments

Comments


bottom of page