Reputation: 1279
I used to logging in C with variable amount of arguments and formatting, and I wanna how Can I meet this in C++.
Through Q&A like this (How to make a variadic macro for std::cout?), I know how to handle variable amount. But what I still do not know is, how to format, cause I can not use methods like 'setbase' between arguments now.
For example:
// in C
#define err(fmt, ...) (printf("[%s] "fmt"\n", __FUNCTION__, ##__VA_ARGS__))
#define FATAL(fmt, ...) do{\
err(fmt, ##__VA_ARGS__);\
CLEAN_UP;\
exit(1);\
}while(0)
int main(){
if(1) FATAL("Just a test: 0x%lX, %d", 1, 2);
return 0;
}
"FATAL" here, accept variable amount of arguments with formatting, print them, and do some extra. I have no idea how to declare such a "FATAL" in C++.
Upvotes: 1
Views: 1777
Reputation: 148890
First, even it often leads to harder to maintain code, you call always use C techniques in C++. stdio.h
functions work natively in C++ and almost all macro are translated the same.
If you want to make use of c++ goodies (better type control at compile time)... you will have to forget old C variadic functions, notably all xprintf
. There may be one interesting part anyway with templates.
Anyway the example given in the referenced Q&A is all you need here. Formatting instructions are simply injected in streams the same values are.
But here is a C++11 example showing that you can do what you want without using any macro. It is much longer than the C macro version, but it looks form me much more clear and extensible without the ugly do { ... } while 0
idom:
#include <iostream>
#include <string>
// disp is a variadic templated function injecting any arguments to a stream
// version for one single arg
template <typename T>
void disp(std::ostream& out, T arg) {
out << arg;
}
// recursively displays every arg
template <typename T, typename ... U>
void disp(std::ostream& out, T arg, U ... args) {
disp(out, arg) ;
disp(out, args...);
}
/* fatal displays its args to std::cout, preceded with "FATAL " and followed
* by a newline.
* It then does some cleanup and exits
*/
template<typename ... T>
void fatal(T ... args) {
std::cout << "FATAL ";
disp(std::cout, args...);
std::cout << std::endl;
// cleanup
exit(1);
}
int main() {
int i = 21;
int j = 32;
std::string s = "foo";
if(1) fatal(1, " " , s, " ab ", i, " 0x", std::hex, j);
return 0;
}
output is
FATAL 1 foo ab 21 0x20
Last but not least, you'd better use a throw FatalException()
where FatalException
is a subclass of std::exception
instead of directly use exit(1)
. You could even write to a stringstream
and pass the resulting string to the exception instead of writing to a real stream, but then you should be prepared to deal with bad_alloc
exceptions.
Upvotes: 0
Reputation: 22670
You can achieve that by using the operator<<
and a custom destructor on an ad-hoc logging object.
class log_error
{
public:
log_error() = default;
log_error(log_error&& other) = default;
~log_error()
{
// Do whatever you want with the input
// Add a timestamp, process/thread id
// Write it to a file, send it to a server ...
std::cerr << "[ERROR] " << ss.str() << std::endl;
throw std::runtime_error(ss.str());
}
std::stringstream ss;
};
template<typename T>
log_error operator<<(log_error&& le, const T& t)
{
le.ss << t;
return std::move(le);
}
I only included the essentials for basic usage. For more complex usage you want to consider a copy variant of the ctor / operator<<
.
The usage is very idiomatic C++. But you have to remember the ()
:
log_error() << "Ooops " << 23 << ", 0x" << std::setbase(16) << 23;
This line will print out the message and throw an exception.
You can customize this however you want. Write to logfiles, add timestamps or other helpful information, verbosity levels and thresholds. It is even possible to have most cases completely optimized out in production builds.
Upvotes: 3
Reputation: 9406
I also have to point out that C++ and C are two different languages with different patterns and idioms. C++ has better alternatives for many C constructs which are more type-safe and thus preferable. IN your case, I would throw an exception in this case. If you ban catch(...)
in your code, it will terminate your program. When the exception is propagated, the compiler will also call destructors of objects and thus do clean-up. If you haven't, I recommend you read up on resource-acquisition-is-initialization (RAII). Since it looks like you are transitioning from C to C++, I recommend to read the tour of C++ which shows fundamental C++ principles. For RAII, the gist is to manage resources in special handler objects which allocate in the constructor and deallocate in the destructor, and implement move semantics. This way, you cannot leak resources. Example implementations are std::vector
, std::unique_ptr
or std::iostream
. As another example, consider mutex locking/unlocking:
class Mutex {
public:
void lock() { ... }
void unlock() { ... }
};
When you use it, it easy to forget unlocking in your code, especially when making modifications to existing code. Also, in case of exceptions, you need try/catch blocks to unlock all the time. Instead, define a MutexLocker
class:
class MutexLocker
{
public:
MutexLocker(std::mullptr_t) = delete;
MutexLocker(Mutex* m): mutex_(m) {mutex_->lock();}
MutexLocker(MutexLocker const&) = delete;
MutexLocker& operator=(MutexLocker const&) = delete;
MutexLocker(MutexLocker&& l): mutex_(l.mutex_) {l.mutex_ = nullptr;}
MutexLocker& operator=(MutexLocker&& l)
{
mutex_ = l.mutex_,
l.mutex_ = nullptr;
return *this;
}
~MutexLocker() {if (mutex_) {mutex_->unlock()} };
private:
Mutex* mutex_;
};
Now, you can never forget to unlock a Mutex. The MutexLocker object cannot be copied, but you can transfer ownership. This is superior to anything you can do in C.
For formatting output, you can google "variadic template printf" which should give you some examples, e.g. on Wikipedia:
void printf(const char *s)
{
while (*s) {
if (*s == '%') {
if (*(s + 1) == '%') {
++s;
}
else {
throw std::runtime_error("invalid format string: missing arguments");
}
}
std::cout << *s++;
}
}
template<typename T, typename... Args>
void printf(const char *s, T value, Args... args)
{
while (*s) {
if (*s == '%') {
if (*(s + 1) == '%') {
++s;
}
else {
std::cout << value;
s += 2; // this only works on 2 characters format strings ( %d, %f, etc ). Fails miserably with %5.4f
printf(s, args...); // call even when *s == 0 to detect extra arguments
return;
}
}
std::cout << *s++;
}
}
Or you can use a library, e.g. boost::format
or probably thousands of other implementations. If it is only for logging, you could take a look at a logging framework, e.g. boost.log
.
Upvotes: 1
Reputation: 1819
C++ is not C! While you can use C-style (and often C) code this is not advisable. Firstly you should not normally rely on macros as they violate the type system, use (possibly inlined or constexpr) functions instead. Then you should not use C-style error handling technique, use exceptions instead. I'd also recommend against variadic arguments in general and finally you don't need C-style string formatting techniques -> this is C++, use stringstreams to format your code.
In your particular case I'd do something like this:
#include <exception>
#include <iostream>
#include <sstream>
#include <string>
inline void fatal(std::string msg) {
// clean_up
throw std::runtime_error(msg);
}
int main(){
std::ostringstream msg;
msg << "Just a test: " << 1 << 2;
if(1) fatal(msg.str());
return 0;
}
Upvotes: 1