Sign in to follow this  
OpOpOpOp

[C++] Is there an easy way to mimic printf()'s behavior?

Recommended Posts

OpOpOpOp    154

Topic title says it all. I would like to turn this mess:

char string[100];
sprintf(string, "Error: %s at %08X with parameter %i and size %.2f", szA, pB, iC, fD);
pretty_fatal_error_thrower(string);

into this beauty:

pretty_fatal_error_thrower("Error: %s at %08X with parameter %i and size %.2f", szA, pB, iC, fD);

Can it be done without basically re-writing printf() from scratch? That's pretty much all I get with a Google search. I would switch to C++ streams except for the minuscule yet important detail that streaming does not support custom formatting such as "%08X". Thanks in advance.

Share this post


Link to post
Share on other sites
Bacterius    13165

Is printf not ok?

 

I think he wants to automatically do other things when that function is called in addition to printf'ing (like wrapping the function) so, yeah, a variadic function with vsprintf - or a variadic macro - would be the usual way to do this in C99 as Hodgman suggested, and of course if you go C++ there's probably plenty of alternatives.

Share this post


Link to post
Share on other sites
Stainless    1875

I used to work at a company called Tao. In the four years I was there, not a single day went by without an active bug being logged on printf. smile.png

 

I would switch to strings and write my own hex formatting code for that one case.

Share this post


Link to post
Share on other sites
Zao    971

Perhaps this would work:

	char str[255];

 

The core problem with using a C-style printf is that you have to either guess at the length of the resulting string or use one of the "safe" sized variants of printf and attempt with larger and larger buffers until the resulting string fits.

 

This is more pretty_fatal_stack_smash than pretty_fatal_message_format.

Share this post


Link to post
Share on other sites
Aardvajk    13207

In addition to Zao's points, you also have no type safety and can easily supply a parameter list that does not match the format string.

 

Here's a really naive header I use to provde type-safe string building. You could extend this easily enough:

#ifndef STRINGFORMAT_H
#define STRINGFORMAT_H

#include <string>
#include <sstream>

template<class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8> std::string stringFormat(const T0 &t0, const T1 &t1, const T2 &t2, const T3 &t3, const T4 &t4, const T5 &t5, const T6 &t6, const T7 &t7, const T8 &t8)
{
    std::ostringstream os;
    os << t0 << t1 << t2 << t3 << t4 << t5 << t6 << t7 << t8;

    return os.str();
}

template<class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7> std::string stringFormat(const T0 &t0, const T1 &t1, const T2 &t2, const T3 &t3, const T4 &t4, const T5 &t5, const T6 &t6, const T7 &t7){ return stringFormat(t0, t1, t2, t3, t4, t5, t6, t7, ""); }
template<class T0, class T1, class T2, class T3, class T4, class T5, class T6> std::string stringFormat(const T0 &t0, const T1 &t1, const T2 &t2, const T3 &t3, const T4 &t4, const T5 &t5, const T6 &t6){ return stringFormat(t0, t1, t2, t3, t4, t5, t6, ""); }
template<class T0, class T1, class T2, class T3, class T4, class T5> std::string stringFormat(const T0 &t0, const T1 &t1, const T2 &t2, const T3 &t3, const T4 &t4, const T5 &t5){ return stringFormat(t0, t1, t2, t3, t4, t5, ""); }
template<class T0, class T1, class T2, class T3, class T4> std::string stringFormat(const T0 &t0, const T1 &t1, const T2 &t2, const T3 &t3, const T4 &t4){ return stringFormat(t0, t1, t2, t3, t4, ""); }
template<class T0, class T1, class T2, class T3> std::string stringFormat(const T0 &t0, const T1 &t1, const T2 &t2, const T3 &t3){ return stringFormat(t0, t1, t2, t3, ""); }
template<class T0, class T1, class T2> std::string stringFormat(const T0 &t0, const T1 &t1, const T2 &t2){ return stringFormat(t0, t1, t2, ""); }
template<class T0, class T1> std::string stringFormat(const T0 &t0, const T1 &t1){ return stringFormat(t0, t1, ""); }
template<class T0> std::string stringFormat(const T0 &t0){ return stringFormat(t0, ""); }

#endif // STRINGFORMAT_H

Usage

class SomeClass
{
};

// implement ostream overload for SomeClass

SomeClass c;
std::string s = stringFormat("There are ", 0.24f, " things and ", c, " works too");

You can just std::cout the result and throw your exception instead of returning the string. I'm sure better, more extensible ways of doing this now with varadic template args exist but they are beyond me at present.

Edited by Aardvajk

Share this post


Link to post
Share on other sites
Here's what I currently use for formatting:
 
/*
	Creates a string from 'str', with every occurrence of "%n" replaced with the nth argument.

	Example: String::Format("The time is now %1 on the %3 of %2", {time, month, day})

	It starts with 1%. 0% is not understood.
*/
std::string Format(std::string text, const std::vector<std::string> &arguments)
{
	std::vector<std::string> symbols = {"%1", "%2", "%3", "%4", "%5", "%6", "%7", "%8", "%9", "%10", "%11", "%12", "%13", "%14", "%15"};

	unsigned int i = 0;
	for(const auto &arg : arguments)
	{
		if(i < symbols.size())
		{
			text = String::ReplaceAll(text, symbols[i], arg);
		}

		i++;
	}

	return text;
}
Just a quick and dirty one without many features (and Schlemiel algorithm'd too - been meaning to rewrite it next time I'm bored) - and I manually convert the arguments to strings when passing them in:
//Converts the version struct to a string like: "2013.07.22.2235"
std::string cDateVersion::ToString(bool includeHourSeconds) const
{
	return String::Format((includeHourSeconds? "%1.%2.%3.%4%5" : "%1.%2.%3"),
                              {std::to_string(this->year),
                              std::to_string(this->month),
                              std::to_string(this->day),
                              std::to_string(this->hour),
                              std::to_string(this->minute)});
}
 
But by mixing variadic templates (like kloffy shows), and an overloaded or templated function for string conversion, you could make a very powerful replacement for sprintf. Edited by Servant of the Lord

Share this post


Link to post
Share on other sites
ChaosEngine    5185


In addition to Zao's points, you also have no type safety and can easily supply a parameter list that does not match the format string.

 

 "Basically whenever you invoke the dread ellipses construct you leave the happy world of type safety." -- SiCrane

 

:D

Share this post


Link to post
Share on other sites
SeanMiddleditch    17565

I used to work at a company called Tao. In the four years I was there, not a single day went by without an active bug being logged on printf. smile.png


... I'm desperately hoping that was just hyperbole and that your company didn't write its own broken printf that had 1460 bugs in it that nobody could fix within 4 years. smile.png

Share this post


Link to post
Share on other sites
Alundra    2316

Check out vsprintf, or for C++, boost::format.

Boost is ok for unit-test but should not be used since it's heavy weight.

In my opinion, vsprintf or use just a lib for that is a better choice.

EDIT: (it's funny like the truth always give bad point :))

Edited by Alundra

Share this post


Link to post
Share on other sites
Alundra    2316

 


EDIT: (it's funny like the truth always give bad point )

 

Its even funnier that you think downratings suggest your view is the truth.

 

I'm not the only one to say that, a lot of professional think the same and it's for that boost is not used.

Edited by Alundra

Share this post


Link to post
Share on other sites
Aardvajk    13207

You don't HAVE to abandon type safety - if you have a decent compiler (GCC, Clang, some others), you can put an attribute on any function that uses printf style format strings, and the compiler will do type checking of the varargs against the string.http://stackoverflow.com/questions/996786/how-to-use-the-gcc-attribute-format

 

Interesting, didn't know about that. As has been said, you can also work around the buffer overflow potential if you are careful (not actually relevant to printf, fprintf etc anyway).

 

The main remaining problem for me though is not solvable with varadic args - support for new types. You instead need to implement .toString() methods on your types as the only sensible alternative.

Edited by Aardvajk

Share this post


Link to post
Share on other sites
OpOpOpOp    154

Wow! Thanks to everyone who replied. Apparently there's a lot of ways to go about this. I'll be trying out some of your methods and see which one fits my project better. Thanks again. :)

Share this post


Link to post
Share on other sites
Alundra    2316
Being a "professional" is completely orthogonal to being right. 

It's true, a lot of person is professional and doesn't know what they really do or the math behind what they do.

It's very the truth when this is person who is just out of school, they have no experience and it's a problem.

I don't say boost is bad, I just say it has to be used in a right way since it's a heavy weight lib.

If it's just for one module, it's too heavy to use boost, alternative exist.

Share this post


Link to post
Share on other sites

Had a few hours to kill, so I wrote this function:

//Ends the variadic template.
template<unsigned int argIndex>
void priv_Format(std::string &input, char symbol, char escapeSymbol)
{
	return;
}

template<unsigned int argIndex, typename Arg, typename... Args>
void priv_Format(std::string &input, char symbol, char escapeSymbol, const Arg &arg, Args &&... args)
{
	//Check how many chars are needed to represent the number 'argIndex'
	unsigned int indexCharSize = NumSymbolsAsString(argIndex);
	
	//Convert the argument to a string.
	std::string argumentAsText = ToString(arg);
	
	//Iterate over the input string.
	char prevChar = '\0';
	for(size_t i = 0; i < (input.size() - indexCharSize); ++i)
	{
		//Check for the character 'symbol', as long as it's not escaped.
		if(input[i] == symbol && prevChar != escapeSymbol)
		{
			//Make sure this symbol's number is the same index as our argument.
			if(std::stoi(std::string(input, i+1, indexCharSize)) == argIndex)
			{
				//We're replacing the symbol and the number after it.
				size_t replacementSize = (1 + indexCharSize);
				
				//Replace the symbol with the argument.
				input.replace(i, replacementSize, argumentAsText);
				
				//Adjust our iterator to compensate for the removal + insertion.
				i -= replacementSize;
				i += argumentAsText.size();
			}
		}
		
		prevChar = input[i];
	}
	
	//Go on to the next argument.
	priv_Format<argIndex+1>(input, symbol, escapeSymbol, std::forward<Args>(args)...);
}

//Iterates over 'input' replacing every occurance of '%n' with the nth argument.
//
//Speed concerns for the next time I'm bored:
//	Iterates over the entire string once per argument.
//	Calls std::string::replace (potentially reallocating the string) once for every symbol
//	in 'input' that has a matching argument (e.g. once for every %n if an argument actually exists for 'n').
template<typename... Args>
std::string Format(const std::string &input, Args &&... args)
{
	std::string output(input);
	priv_Format<1>(output, '%', '\\', std::forward<Args>(args)...);
	
	return output;
}

 
Which given this:

int main(void)
{
	std::cout << Format("Test") << std::endl;
	std::cout << Format("Today is %2/%1/%3", 9, 4, 2014) << std::endl; //Arguments don't have to be in order (useful for localization).
	std::cout << Format("Today is %2 %1, %3", std::string("9th"), "April", 2014) << std::endl; //Mix and match argument types.
	std::cout << Format("%1 %2 %3 %4 %3 %2 %1", 'W', 'X', 'Y', 'Z') << std::endl; //Arguments can be repeated in the string.
	std::cout << Format("Position: %2 at a distance of %1", 4.057f, CustomType{357,100}) << std::endl; //Support for custom types by overloading ToString().
	
	return 0;
}

Outputs this:
[tt]Test

Today is 4/9/2014
Today is April 9th, 2014
W X Y Z Y X W
Position: (357, 100) at a distance of 4.057000[/tt]

 

[Ideone example]

 

For additional formatting of arguments, you could format them when you pass them in:

//Converts an integer to a std::string, in hexadecimal format. 'digits' is the fixed number of digits that show up.
//For example, 0x00FF00AA shows zeroes if 'digits' is 8, and only shows 0x00AA, if 'digits' is 4.
std::string UnsignedIntToHexString(uint32_t value, const std::string &prefix = "0x", size_t digits = 8, bool uppercase = true);

//Apparently I don't (yet) have a float-to-string conversion function that lets you fine-tune the precision.

int main()
{
    char *errorMessage = ...;
    float size = ...;
    unsigned int address = ...;

    String::Format("Error: %1 at %3 with parameter %2 and size %4", errorMessage, 54, UnsignedIntToHexString(address, "0x", 8), size);
}

But it would be nice (and helpful for localization) to be able to directly pass into the input string additional formatting information... I'm thinking that ToString() could be expanded to handle that, and could be passed in additional data like "This text %1[.2F] has formatted data", with the text in the square brackets (".2F") being passed in to ToString() as a second argument, or something like that.

 

In other cases, the custom types passed in should maybe change their formatting based on the locale. Like dates and times, to have different spellings for different languages ("January" -> "Enero" (spanish)), and different layouts (day/month/year, month/day/year, etc...) - the overloads of ToString() for custom types could handle that (calling String::Format() for the date orderings, or looking up in a map the month names).

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this