boost.log std::异常格式化程序找不到运算符<< 自己的命名空间中的重载

boost.log std::exception formatter unable to find operator<< overload in own namespace

本文关键字:lt 自己的 重载 命名空间 运算符 std log 异常 格式化 找不到 程序      更新时间:2023-10-16

我为boost.log创建了一个简单的格式化程序,如本例中std::exception所示。现在,如果我想使用重载运算符,它是在我自己的命名空间中定义的,那么日志无法找到重载。

部分代码:

namespace my_space {
template< typename CharT, typename TraitsT >
std::basic_ostream< CharT, TraitsT >& operator<< (std::basic_ostream< CharT, TraitsT >& strm, std::exception const& e) {
    // some printout stuff here
    strm << e.what();
    return strm;
}
} // namespace my_space

但如果我将重载移到std命名空间中(Stroustrup请不要射杀我,这只是为了测试(,格式化程序会找到它。

错误消息在formatting_ostream.hpp(boost 1.59.0第782行(中

template< typename StreamT, typename T >
inline typename boost::log::aux::enable_if_formatting_ostream< StreamT, StreamT& >::type
operator<< (StreamT& strm, T const& value)
{...}

与Visual Studio 2013的信息说:

Error   818 error C2679: binary '<<' : no operator found which takes a right-hand operand of type 'const std::exception' (or there is no acceptable conversion) d:prgboost1.59.0includeboostlogutilityformatting_ostream.hpp

我的意图是,我有一个自己的异常类(在命名空间My_space中定义(,它继承自std::exception,所以我可以抛出自己的异常,但捕获std::异常。

using namespace my_space;
try {
    // throw(std::runtime_error("something happend."));
    throw(my_exception(0x1, "something happend."));
}
catch (std::exception& e) {
    std::cerr << e << std::endl;  // works just fine
    MY_LOG_ERROR(slg) << log::add_value("Exception", e); // compile error
}

如何在不使用我自己的函数/重载或创建双捕获块污染std命名空间的情况下实现这一点?

您的问题中有两个问题,我将在下面分别解决。

1.名称查找

在C++中,非限定函数调用(如流表达式中的operator<<(涉及非限定名称查找,这基本上会生成一组可能正在调用的候选函数。然后根据过载解决规则从该集合中选择实际函数。为了完成调用,必须(a(目标函数在候选集合中,(b(考虑到函数的调用方式(提供的参数数量及其类型、显式模板参数等(,该函数相对于集合中的其他函数不含糊。

简单地说,operator<<查找分三个阶段执行。首先,编译器在右手操作数类中查找成员operator<<。Boost.Log流中定义的运算符就是这样找到的。接下来,从内部名称空间开始向外移动,在包含函数调用的名称空间中查找一个独立的(非成员(运算符。这里还考虑使用using指令和声明导入的名称。一旦找到任何operator<<,该查找就结束。请注意,当您从代码中调用运算符时,以及当您使用log::add_value时,从Boost.Log调用运算符时所考虑的命名空间是不同的。在第一种情况下,您将my_space导入到当前的命名空间中,这样就可以找到您的运算符。在后一种情况下,Boost.Log不会导入您的命名空间,因此找不到您的运算符。

最后,编译器执行参数相关查找(ADL(,以收集您可能正在调用的其他函数。基本上,它在相关的名称空间中查找运算符,这些名称空间包括:

  • 声明函数调用中每个参数的类型的命名空间。这意味着在这两种情况下都考虑名称空间std,因为std::exception是在那里声明的。当使用Boost.Log时,也会考虑其声明流类型的内部命名空间(它包含一些运算符,但没有一个接受std::exception(
  • 如果函数是一个模板,那么它的模板参数类型的名称空间也会被类似地考虑
  • 如果函数参数类型或函数模板参数类型本身就是模板,则同样会考虑这些模板参数的命名空间。这是递归完成的

ADL在名称空间std中找到operator<<,但它们都不接受std::exception。这里运算符查找的净效果是,my_space中的运算符是因为using-指令才被找到的,当从程序的另一个点(如Boost.Log代码(调用运算符时,这没有帮助。

在实现运算符时,最佳实践是依靠ADL找到这些运算符。这意味着支持某个类型的运算符必须放在声明该类型的同一命名空间中。在std::exception的情况下,这是名称空间std。从技术上讲,向命名空间std添加内容会导致未定义的行为(根据[namespace.std]/1(,因此最好的解决方案是在命名空间中定义自己的流操纵器,并使用它将异常输出到流中:

namespace my_space {
template< typename T >
struct my_manip
{
  T const& value;
};
template< typename T >
my_manip< T > to_stream(T const& value) {
  my_manip< T > m = { value };
  return m;
}
template< typename CharT, typename TraitsT >
std::basic_ostream< CharT, TraitsT >& operator<< (
  std::basic_ostream< CharT, TraitsT >& strm,
  my_manip< std::exception > const& e)
{
  // some printout stuff here
  strm << e.value.what();
  return strm;
}
} // namespace my_space
try {
  // ...
}
catch (std::exception& e) {
  std::cerr << my_space::to_stream(e) << std::endl;
}

如果愿意,还可以为my_manip提供一个通用的operator<<

2.添加Boost.Log属性

如果你的初衷是将异常附加到日志记录中,那么恐怕你做得不对。add_value操纵器不会确定您提供的值的运行时类型,这意味着它会保存std::exception的副本,从而丢失派生类提供的任何诊断信息(包括what()消息(。这就是所谓的对象切片。

你可以走多种途径来实现你想要的。首先,您可以在捕获异常的地方格式化错误消息。

catch (std::exception& e) {
  MY_LOG_ERROR(slg) << e.what();
}

您将无法在接收器或格式化程序中将其用作属性值,但这对您来说可能已经足够了。当然,您也可以使用自定义操纵器:

catch (std::exception& e) {
  MY_LOG_ERROR(slg) << my_space::to_stream(e);
}

如果你确实需要它作为属性值,你必须选择你想要的信息类型。例如,如果您只需要错误消息,您可以附加它而不是异常:

catch (std::exception& e) {
  MY_LOG_ERROR(slg) << log::add_value("ErrorMessage", std::string(e.what()));
}

如果异常本身就是您所需要的,那么您需要C++11 exception_ptr:

catch (std::exception& e) {
  MY_LOG_ERROR(slg) << log::add_value("Exception", std::current_exception());
}

在C++03中,您可以使用Boost.Exception或实现确定异常动态类型的自定义机制。请注意,标准库或Boost.Exception也没有为exception_ptr提供operator<<,因此您必须为此实现自定义格式化程序。