Unicode的异常what()。

65

或者说,“俄罗斯人如何抛出异常?”

std::exception的定义是:

namespace std {
  class exception {
  public:
    exception() throw();
    exception(const exception&) throw();
    exception& operator=(const exception&) throw();
    virtual ~exception() throw();
    virtual const char* what() const throw();
  };
}

设计异常层次结构的流行思想之一是从std::exception派生:

通常最好抛出对象,而不是内置类型。如果可能,应该抛出继承自std::exception类(最终)的类的实例。通过使您的异常类(最终)继承标准异常基类,您可以为用户简化生活(他们可以通过std::exception捕获大多数内容),并且您可能会为他们提供更多信息(例如,您特定的异常可能是std::runtime_error或其他异常的细化)。

但面对Unicode,似乎不可能设计一个异常层次结构来同时实现以下两点:

  • 最终派生自std::exception以便在catch语句中使用方便
  • 提供Unicode兼容性,以便诊断信息不会被切割或变成乱码

设计一个可以使用Unicode字符串构造的异常类非常简单。但标准规定what()必须返回const char*,因此输入字符串必须在某个时候转换为ASCII。无论是在构造时还是在调用what()时进行转换(如果源字符串使用的字符不能表示为7位ASCII),都可能无法格式化消息而不失真。

如何设计异常层次结构,将std::exception派生类的无缝集成与无损Unicode诊断相结合?


1
没什么大不了的,只需使用一个使用字节的编码即可。在我看来,std::exception 的更大问题是派生类从中非虚拟派生。由于这个原因,你无法从自己的基类派生,该基类又从 std::exception 派生,然后再从 std::out_of_range 派生。 - sbi
@sbi:确实如此,但我通过直接使用std::exception来定义我的继承关系来避免这个问题。我抛出自己派生的std::exception异常,并将其他标准定义的异常留给标准库。虽然不是理想的解决方案,但对于我的用途来说,考虑到标准的当前状态,这是最好的解决方案。 - John Dibling
1
刚刚注意到:似乎是重复的问题:https://dev59.com/23RB5IYBdhLWcg3wc280#618150 - Nemanja Trifunovic
9
在苏联,异常抛出你。 - Marc.2377
9个回答

36

char* 并不意味着 ASCII。你可以使用8位 Unicode 编码,如 UTF-8。char 也可以是16位或更多,那么你可以使用 UTF-16。


1
选择UTF-8路径的额外好处是STL等异常文本字符串已经是有效的UTF-8。问题在于,一旦超过7位代码点,处理起来有些麻烦。此时,您需要为UTF-8编写自定义输出例程或转换例程到8位或16位代码页,这些都可能是您不想在异常处理程序中执行的操作。 - Andreas Magnusson
3
@Andreas:使用std::string处理UTF-8字符串存在两个问题:一是UTF-8中,字符串的字符数和字节数不同;二是很容易混淆系统编码的字符串(每个应用程序都需要)和UTF-8编码的字符串,导致用户看到奇怪的文本。我发现最好使用例如std::basic_string<signed char>来处理UTF-8编码的字符串。这样至少可以消除第二个问题,因为当您混淆编码时编译器会报错提醒您。 - sbi
3
系统编码的字符串中使用ASCII子集之外的字符有多普遍?如果可以将系统编码的字符串限制为ASCII子集,则可以使用UTF-8而不会出现奇怪的文本。至于字符串长度,我喜欢使用std::string,因为我可以从中获取字节计数,并可以在O(n)时间内计算字符数。基本上,如果您想让字符串按字符思考,您必须子类化std::basic_string<signed char>,更改其迭代器(可能降级为非随机访问迭代器),并添加一个字节计数方法。 - Mike DeSimone
@sbi:我想你误解了我的意思,我的意思是what()返回的文本字符串对于stdlib异常已经是有效的UTF-8字符串,因为它们是ASCII,而ASCII是UTF-8的子集。此外,我将您的两个问题合并成了一个大的“笨重的问题”,因为所有与UTF-8有关的问题都始于移出ASCII子集。说到解决方案,我非常喜欢由下面的ybungalobill发布的帖子中所接受的答案。 - Andreas Magnusson

10

返回UTF-8是一个明显的选择。然而,如果使用您的异常的应用程序使用不同的多字节编码,它可能很难显示字符串。(它不能知道它是UTF-8,对吗?) 另一方面,对于ISO-8859-* 8位编码(西欧、西里尔文等),显示UTF-8字符串将“只”显示一些无意义的字符,如果您无法区分本地字符集中的char*和UTF-8,则您(或您的用户)可能会满意。

个人认为,只有低级别的错误消息应该放入what()字符串中,而且我个人认为这些消息应该是英文的。(也许结合一些错误号或其他什么东西。)

我看到的最糟糕的问题是,what()中通常包含一些上下文详细信息,例如文件名。文件名经常是非ASCII字符,因此您别无选择,只能使用UTF-8作为what()的编码方式。

请注意,您的异常类(派生自std::exception)可以提供任何您喜欢的访问方法,因此添加一个明确的what_utf8()what_utf16()what_iso8859_5()可能是有意义的。

编辑: 关于John的评论如何返回UTF-8:

如果你有一个const char* what()函数,它实际上返回一堆字节。在西欧的Windows平台上,这些字节通常编码为Win1252,但在俄罗斯的Windows上,它可能是Win1251
字节返回的含义取决于它们的编码方式,而它们的编码方式取决于它们的“来源”(以及谁来解释它们)。字符串文字的编码在编译时定义,但在运行时,仍然由应用程序决定如何解释这些文本。
因此,要使异常回复UTF-8字符串与what()(或what_utf8())一起使用,您必须确保:
  • 异常的输入消息具有明确定义的编码
  • 您使用的字符串成员具有明确定义的编码。
  • 当调用what()时适当地转换编码
例如:
struct MyExc : virtual public std::exception {
  MyExc(const char* msg)
  : exception(msg)
  { }
  std::string what_utf8() {
    return convert_iso8859_1_to_utf8( what() );
  }
};

// In a ISO-8859-1 encoded source file
const char* my_err_msg = "ISO-8859-1 ... äöüß ...";
...
throw MyExc(my_err_msg);
...
catch(MyExc const& e) {
  std::string iso8859_1_msg = e.what();
  std::string utf_msg = e.what_utf8();
...

转换也可以放在MyExc()的(重载)what()成员函数中,或者您可以定义异常以使用已经UTF-8编码的字符串,或者您可以在构造函数中进行转换(从预期输入编码,可能是wchar_t/UTF-16)。


“返回UTF-8是一个显而易见的选择。” 这似乎遵循当前思路的趋势。现在唯一的问题是,我如何返回UTF-8? :) - John Dibling
@John Dibling:如果你的消息文本全部用英语书写,并且可以使用标准ASCII表示,那么你已经完成了足够的工作,因为ASCII和UTF-8的前128个字符是相同的。如果你使用的字符和编码高于127,那么你需要将编码转换成UTF-8。目前肯定有一个标准的C++库函数可以实现这一点。如果没有,libiconv也可以实现这一点。 - JeremyP
2
@JeremyP:我工作的地方使用ICU来处理Unicode,虽然不是完美的(C接口...),但它能够胜任工作并处理Unicode / 国际化 / 本地化的怪癖。 - Matthieu M.
@Matthieu M:谢谢你。我正在寻找一个兼容C的Unicode库。我本可以使用libiconv,但它的许可证更加限制。 - JeremyP
@JeremyP:很高兴能帮忙 :) - Matthieu M.

4
第一个问题是,您打算如何处理what()字符串? 如果您计划在某个地方记录信息,那么您不应该使用what()字符串的内容,而是使用该字符串作为参考,查找正确的本地特定日志消息。因此,对我来说,what()的内容不适用于记录目的(或任何形式的显示),它是查找实际日志字符串的一种方法(可以是任何Unicode字符串)。
现在,what()字符串中包含人类可读的消息对于开发人员以帮助快速调试很有用(但对于这种高度可读的精美文本不是必需的)。因此,没有理由支持除ASCII之外的任何内容。 遵循KISS原则。

针对您的问题,我想使用what()字符串来生成两个级别的诊断。较低级别是开发人员或技术人员为中心的诊断,将显示在日志文件中。但在更高的级别上,我希望这些字符串被用于构建一个普通人可以操作的诊断。正如您所暗示的那样,what()返回值可以简单地是到更人性化消息表格的查找值,但字符串的某些组件(或至少是异常)需要是可读的,例如“文件blah.txt无法找到”。 - John Dibling
我另一个目标是尽量减少catch块的使用。理想情况下,只需要一个catch(const std::exception& ex)块来捕获所有异常,并且该块将消耗what()字符串以生成技术和人类级别的诊断信息。按照这种模式,构造两个消息所需的所有数据都必须从what()字符串中检索。 - John Dibling
大多数本地化转换语言都会接受一个输入字符串,并通过资源将其转换为本地字符串。因此,如果您说字符串的第一部分直到冒号被用来查找本地字符串,那么您可以这样做:File could not be found: blah.txt。然后,可以使用“File could not be found:”部分来查找本地特定的翻译。 - Martin York
这突显了标准库异常设计的基本缺陷:它们应该被设计成可以用整数常量初始化,而不是char const * - Spencer

3

在错误处理中添加Unicode是更好的方式:

try
{
   // some code
}
catch (std::exception & ex)
{
    report_problem(ex.what())
}

并且:

void report_problem(char const * const)
{
   // here we can convert char to wchar_t or do some more else
   // log it, save to file or message to user
}

3
一个 const char* 不一定指向 ASCII 字符串;它可以是多字节编码,如 UTF-8。一种选择是使用 wcstombs() 等函数将 wstring 转换为字符串,但在打印之前可能需要将 what() 的结果转换回 wstring。这还涉及比您在异常处理程序中感到舒适的更多复制和内存分配。
我通常只定义自己的基本异常类,在构造函数中使用 wstring 而不是 string,并从 what() 返回 const wstring&。这并不是很麻烦。缺乏标准异常类是一个相当大的疏忽。
另一个有效的观点是,异常字符串永远不应呈现给用户,因此不需要对其进行本地化,因此您不必担心上述任何问题。

在我看来,创建自己的异常类是最合理的做法。如果捕获std::exception,如果编码未知(是CP1252还是UTF-8?)则无法处理。如果您有自己的异常类,则问题得到解决。 - Arnaud

2

1

what()通常不用于向用户显示消息。除此之外,它返回的文本是不可本地化的(即使它是Unicode)。我建议你只使用what()来显示对开发人员有价值的内容(例如引发异常的源文件和行号),而对于这种文本,ASCII通常已经足够了。


这是你的观点,虽然我尊重你的看法,但我不同意。即使what()输出仅存储到日志文件中,在某种程度上也需要“呈现给用户”,并且不能是无意义的废话。 - John Dibling
1
我并不是说它应该是无意义的。我是说,what() 不适合保存“国际化”文本,不是因为它不能保存 Unicode(它可以),而是因为它不可本地化。 - Nemanja Trifunovic
3
当然,例外文本可能不需要像用户通常看到的文本那样进行“国际化”。但我可以想象,在某些情况下,Unicode文本仍然非常相关,并且希望将其包含在异常中。例如,文件名或路径可能包含Unicode字符。如果将其省略,则异常处理或日志记录将变得不太有用。 - TheUndeadFish
1
为什么您不能国际化它?您无法在 what 中访问本地内容吗? - Matthieu M.

0

6
我认为在没有解释如何将其与C++异常相关联的情况下添加一个链接(顺便说一下,这是一个很棒的链接)无助于回答问题。(它可能有助于将一些编码问题放置在上下文中,但这就是评论的作用,不是吗?)如果提问者确实需要阅读链接,那么这就更加重要了。 - Martin Ba
4
此外,我已经阅读了链接,但它并没有回答我的问题。 - John Dibling
2
相反,我认为这个链接提供了非常好的见解,说明为什么使用char const*与字符编码无关。 - Alexandre C.
4
@Alexandre: 但对于在SO上的读者来说,没有任何提示告诉我为什么要阅读这篇外部网站的长文。正如@Martin所说,不要只发布链接,还要发布简短摘要和/或解释为什么该链接很重要。 - jalf

0
我创建了一个模板异常类wexception,它接受wstring消息,将它们存储为UTF-8编码的string,并在检索时将其转换回wstring。代码中包含了一些特定于MS C++的部分:std::exception::exception(char*)构造函数以及用于转换为和从UTF-8的MultiByteToWideChar和WideCharToMultiByte函数,以及declspec(selectany)指令,该指令指示链接器只创建一个已定义多次的符号的实例。模板参数可以是任何具有以char*参数为输入的构造函数的异常类。代码不使用概念(concepts),以便在较旧的语言标准下编译。还有改进的空间。
#include <Windows.h> // for UTF8 conversion functions and constants
#include <exception>
#include <string>

class wexception_base
{
public:
    static std::wstring wwhat(std::exception const& e)
    {
        return FromUtf8OrAnsi(e.what());
    }
    static bool IsUtf8Bom(char const* pmsg)
    {
        if (pmsg == nullptr)
            return false;
        for (size_t i = 0; i < sizeof(m_Utf8Bom); ++i)
            if (pmsg[i] != m_Utf8Bom[i])
                return false;
        return true;
    }
    static std::wstring FromUtf8OrAnsi(char const* pmsg)
    {
        if (pmsg == nullptr)
            return {};
        size_t len = strlen(pmsg);
        bool const isUtf = IsUtf8Bom(pmsg);
        if (isUtf)
        {
            pmsg += sizeof(m_Utf8Bom);
            len -= sizeof(m_Utf8Bom);
            if (len == 0)
                return {}; // Quick exit for empty message
        }
        int const cp = isUtf ? CP_UTF8 : CP_ACP;
        size_t required = MultiByteToWideChar(cp, 0, pmsg, -1, nullptr, 0);
        _ASSERT(required > 0);
        _ASSERT(required <= INT_MAX);
        if (required > INT_MAX - 16) // why 16? probably 2 would be enough
            required = INT_MAX - 16;
        std::vector<wchar_t> buffer(required + 1);
        auto const used = MultiByteToWideChar(cp, 0, pmsg, -1, buffer.data(), static_cast<int>(buffer.size()));
        _ASSERT(used > 0);
        return buffer.data();
    }
    static std::string ToUtf8(wchar_t const* pmsg)
    {
        size_t required = WideCharToMultiByte(CP_UTF8, 0, pmsg, -1, nullptr, 0, nullptr, nullptr);
        _ASSERT(required > 0);
        _ASSERT(required <= INT_MAX);
        if (required >= INT_MAX)
            return {};
        std::vector<char> buffer(required + sizeof(m_Utf8Bom) + 1);
        memcpy_s(buffer.data(), required + 3, m_Utf8Bom, sizeof(m_Utf8Bom));
        auto const used = WideCharToMultiByte(CP_UTF8, 0, pmsg, -1, buffer.data() + sizeof(m_Utf8Bom), static_cast<int>(buffer.size()), nullptr, nullptr);
        _ASSERT(used > 0);
        return buffer.data();
    }
    static char m_Utf8Bom[3];
}; // class wexception_base
/*static*/ __declspec(selectany) char wexception_base::m_Utf8Bom[3] = {'\xEF', '\xBB', '\xBF'};

template<class BASE = std::exception>
class wexception : public BASE, public wexception_base
{
public:
    using BASE::BASE;
    template<typename... ARGS>
    wexception(wchar_t const* message, ARGS&&... args) : BASE(ToUtf8(message).c_str(), std::forward<ARGS>(args...))
    {
        static_assert(std::is_base_of_v<std::exception, BASE>, "BASE must be std::exception, or derived from std::exception");
    }
    template<typename... ARGS>
    wexception(std::wstring const& message, ARGS&&... args) : BASE(ToUtf8(message.c_str()).c_str(), std::forward<ARGS>(args)...)
    {
        static_assert(std::is_base_of_v<std::exception, BASE>, "BASE must be std::exception, or derived from std::exception");
    }
    template<typename... ARGS>
    wexception(std::string const& message, ARGS&&... args) : BASE(message.c_str(), std::forward<ARGS>(args)...)
    {
        static_assert(std::is_base_of_v<std::exception, BASE>, "BASE must be std::exception, or derived from std::exception");
    }
}; // template class wexception

你可以将 wexception 的实例作为任何异常类来抛出和捕获。捕获语句的写法如下:
    catch (std::exception const& e)
    {
        std::wstring msg = wexception<>::wwhat(e);
        std::wcerr << msg << std::endl;
        return; // or whatever
    }

静态函数 wwhat 通过 BOM(字节顺序标记)识别 UTF-8 字符串,并假设不以 BOM 开头的字符串为 ANSI 编码。无论如何,您将获得一个带有异常消息的 wstring

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接