【问题标题】:C++ variable amount of arguments with formatting for std::coutC++ 可变数量的参数与 std::cout 的格式
【发布时间】:2016-03-05 07:07:48
【问题描述】:

我曾经使用可变数量的参数和格式登录 C,我想如何在 C++ 中满足这一点。

通过这样的问答(How to make a variadic macro for std::cout?),我知道如何处理可变金额。但是我仍然不知道如何格式化,因为我现在不能在参数之间使用像'setbase'这样的方法。

例如:

// 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;
}

“致命”在这里,接受可变数量的带有格式的参数,打印它们,并做一些额外的事情。我不知道如何在 C++ 中声明这样的“致命”。

【问题讨论】:

  • 不要试图在 C++ 中实现 C 风格的代码。在 C++ 中你应该依靠异常来报告错误。然后在 C++ 中通常应该避免使用宏 - 更喜欢普通函数。
  • 我想像在 C 中那样使用没有宏的格式打印变量参数,这就是为什么我要求使用 C++ 风格的方式。 @MarinosK
  • 那么,您想使用您在 C 中使用的技术,尽管 C++ 中通常推荐各种技术,因为它们类型安全(除其他外)并且被认为更可取吗?
  • 为什么不能使用setbase
  • 如果您的示例无关紧要,请删除它。添加相关示例。

标签: c++ c++11


【解决方案1】:

您可以通过使用 operator<< 和对 ad-hoc 日志记录对象的自定义析构函数来实现。

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);
}

我只包括了基本使用的必需品。对于更复杂的用法,您需要考虑 ctor / operator&lt;&lt; 的副本变体。

用法是非常惯用的 C++。但是你要记住()

log_error() << "Ooops " << 23 << ", 0x" << std::setbase(16) << 23;

这一行将打印出消息并抛出异常。

您可以随意自定义。写入日志文件、添加时间戳或其他有用信息、详细级别和阈值。甚至可以在生产版本中完全优化大多数情况。

Live example

【讨论】:

  • @hxpax 你能解释一下这不能满足你的要求吗?
  • 它被异常卡住了。有时我只想记录错误消息;有时我想记录消息并执行除退出之外的一些额外操作,例如 {log;第一幕; act2;},这可能会被多次调用,所以我想将它们包装在一起。
  • 无论你的anwser如何好且信息量大,我从中学到了很多,但Serge的回答在这里更胜任。
  • @hxpax,你可以在析构函数中做任何你想做的事情。您不必使用异常。我希望从评论中可以清楚地看到这一点。
【解决方案2】:

C++ 不是 C!虽然您可以使用 C 风格(通常是 C)代码,但不建议这样做。首先,您通常不应该依赖宏,因为它们违反了类型系统,而是使用(可能是内联或 constexpr)函数。那么你不应该使用 C 风格的错误处理技术,而是使用异常。我还建议一般不要使用可变参数,最后你不需要 C 风格的字符串格式化技术 -> 这是 C++,使用字符串流来格式化你的代码。

在你的特殊情况下,我会做这样的事情:

#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;
}

【讨论】:

  • 即使没有错误也有格式化的代价。
  • 不,它没有,这是一个愚蠢的例子 - 在生产代码中,您只会在某些 catchif 子句中格式化您的代码,并且仅在发生某些错误时.. 没有什么不同而不是在函数内部进行格式化。
  • @Yakk 我的示例仅用于演示,基本上是原始问题中代码的 C++ 版本。
  • 无一例外有什么办法吗?我想在低级编码中避免它们,因为在我的选择中它们有点慢。
  • @hxpax 异常对于正常的非抛出执行路径的开销为零。抛出它们时会有开销,但这无关紧要,因为这种情况是异常的,因此不会影响您的性能。这对于硬实时系统来说可能是个问题,因为您无法保证在异常路径中的运行时间。
【解决方案3】:

我还必须指出,C++ 和 C 是两种不同的语言,具有不同的模式和习语。 C++ 为许多 C 结构提供了更好的替代方案,这些结构更安全,因此更可取。在你的情况下,我会在这种情况下抛出一个异常。如果您在代码中禁止catch(...),它将终止您的程序。当传播异常时,编译器还将调用对象的析构函数,从而进行清理。如果您还没有,我建议您阅读资源获取即初始化 (RAII)。因为看起来您正在从 C 过渡到 C++,所以我建议阅读 the tour of C++,它展示了基本的 C++ 原则。对于 RAII,其要点是管理特殊处理程序对象中的资源,这些处理程序对象在构造函数中分配,在析构函数中释放,并实现移动语义。这样,您就不会泄漏资源。示例实现是std::vectorstd::unique_ptrstd::iostream。再举一个例子,考虑互斥锁/解锁:

class Mutex {
public:
   void lock() { ... }
   void unlock() { ... }
};

使用它时,很容易忘记在代码中解锁,尤其是在修改现有代码时。此外,如果出现异常,您需要 try/catch 块来一直解锁。相反,定义一个MutexLocker 类:

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_;
};

现在,您永远不会忘记解锁互斥锁。 MutexLocker 对象无法复制,但您可以转让所有权。这比你在 C 中可以做的任何事情都要好。

对于格式化输出,你可以谷歌“variadic template printf”,它应该会给你一些例子,例如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++;
    }    
}

或者您可以使用库,例如boost::format 或可能有数千个其他实现。如果仅用于日志记录,您可以查看日志记录框架,例如boost.log.

【讨论】:

  • 宏“致命”不是重点。我只想打印可变数量的参数,并在案例中格式化其中的一些。
  • @hxpax 答案见第二段。
  • @hxpax 将您的示例调整为您真正想要的也很好,因为它会令人困惑。将其剥离为没有 FATAL 等的最小代码段。
  • 谢谢,我已经使用 Log4Cplus 几个月了,也许我不应该尝试以 C 方式使用 C++。
  • 这个 FATAL 做了很好的日志记录和额外的操作,这就是我把它放在这里的原因。
【解决方案4】:

首先,即使这通常会导致代码更难维护,您也可以在 C++ 中调用始终使用 C 技术。 stdio.h 函数在 C++ 中本机工作,几乎所有宏的翻译都相同。

如果你想使用 c++ 的好东西(编译时更好的类型控制)......你将不得不忘记旧的 C 可变参数函数,尤其是所有 xprintf。无论如何,模板可能有一个有趣的部分。

无论如何,参考问答中给出的示例就是您所需要的。格式化指令被简单地注入到相同值的流中。

但这里有一个 C++11 示例,表明您可以在不使用任何宏的情况下做您想做的事。它比 C 宏版本长得多,但它看起来更加清晰和可扩展,没有丑陋的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;
}

输出是

FATAL 1 foo ab 21 0x20

最后但同样重要的是,您最好使用throw FatalException(),其中FatalExceptionstd::exception 的子类,而不是直接使用exit(1)。您甚至可以写入 stringstream 并将结果字符串传递给异常,而不是写入真正的流,但是您应该准备好处理 bad_alloc 异常。

【讨论】:

  • 工作,但比宏有点冗长。
猜你喜欢
  • 2015-06-02
  • 1970-01-01
  • 2012-03-03
  • 1970-01-01
  • 2017-08-04
  • 1970-01-01
  • 1970-01-01
  • 2019-01-18
  • 2010-12-12
相关资源
最近更新 更多