使用MultiByteToWideChar函数和wstring类型的代码安全吗?

3

我正在使用MultiByteToWideChar,可以用std::wstring吗?

std::wstring widen(const std::string &in)
{
    int len = MultiByteToWideChar(CP_UTF8, 0, &in[0], -1, NULL, 0);
    std::wstring out(len, 0);
    MultiByteToWideChar(CP_UTF8, 0, &in[0], -1, &out[0], len);
    return out;
}

不检查winapi函数的返回值总是一个坏主意。如果输入字符串实际上不包含utf-8编码的字符串,那么你肯定无法摆脱它,这只会产生垃圾。 - Hans Passant
@HansPassant 我应该检查两个返回值,还是只检查第一个就可以了? - Josh
2
@Hans:MultiByteToWideChar在出现错误时会返回0。因此,剩余的代码将生成一个空字符串(而不是“垃圾”)。检查返回值是个好主意,即使在这种情况下不这样做也不会造成任何问题(除了无法区分空字符串和错误模式之外)。 - IInspectable
1
确实,它忘记了提到使用MB_ERR_INVALID_CHARS标志。一个空字符串也可以说是垃圾。而且很难诊断这种垃圾。 - Hans Passant
1
阅读这个问题的答案(以及评论)让我想知道有多少人不得不实现这种函数(很多),以及其中有多少实现是有缺陷的(很多,包括我的)。这值得写一篇博客文章。 - Frerich Raabe
显示剩余2条评论
4个回答

5

您第一次调用 MultiByteToWideChar 时出现了问题:字符序列不能保证以零结尾(尽管在实践中通常是这样的)。将该行更改为

int len = MultiByteToWideChar(CP_UTF8, 0, in.c_str(), -1, NULL, 0);

你应该是安全的。即使 MultiByteToWideChar 失败并返回 0,这也可以通过将 len 作为第二次调用 MultiByteToWideChar 的最后一个参数来解决。

话虽如此,从某种意义上说,它是安全的,因为它不会崩溃或破坏内存。然而,还有一个问题:除非输入字符串导致 MultiByteToWideChar 失败,否则返回的字符串将声称其 size() 比它应该的字符多一个。我建议按以下方式更改代码:

std::wstring widen(std::string const &in)
{
    std::wstring out{};

    if (in.length() > 0)
    {
        // Calculate target buffer size (not including the zero terminator).
        int len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
                                      in.c_str(), in.size(), NULL, 0);
        if ( len == 0 )
        {
            throw std::runtime_error("Invalid character sequence.");
        }

        out.resize(len);
        // No error checking. We already know, that the conversion will succeed.
        MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
                            in.c_str(), in.size(), &out[0], out.size());
                            // Use out.data() in place of &out[0] for C++17
    }

    return out;
}

该实现解决了以下问题:
  • 如果输入序列无效的UTF-8,通过传递MB_ERR_INVALID_CHARS标志来报告错误。
  • 使用抛出异常报告错误。这使得可以区分转换错误和成功调用返回零大小字符串的情况。(注意:std::wstring构造函数在失败时已经抛出异常。对于其他错误不抛出异常会感觉不自然。)
  • 该实现正确处理包含嵌入的NUL字符的输入。这很少使用,但是当它被使用时(例如,当组成OPENFILENAMElpstrFilter成员时),它不会因此(悄悄地)失败。
  • 它不会过度分配返回值的容器存储空间。如果在调用MultiByteToWideChar时将cbMultiByte参数设置为-1,则返回的长度包括零终止符的空间。但是,该字符属于std::string实现,而不是要转换的字符序列的一部分。
  • 与前一个项目相关,该实现不会转换零终止符。原始代码这样做,并且当调用c_str()成员时,返回的字符串在末尾产生2个NUL字符。

为什么人们不传递输入字符串的长度?这可以避免太大的问题。 - Mooing Duck
@Mooning:已经适当修改了代码。感谢您的评论。 - IInspectable
当输入一个空字符串时,这段代码似乎会抛出一个错误。也许它应该返回一个空的std::wstring呢? - user673679
@use: API调用返回写入的字符数。如果传入一个空字符串,它将返回1 - IInspectable
@IInspectable 如果提供了大小,则该函数不会写入空终止符,因此返回值为0而不是1。文档还指出:“请注意,如果cbMultiByte为0,则函数将失败。”(即返回0) - user673679
@使用:你是对的。我已经改变了实现方式,使其在传递空字符串时不会抛出异常。 - IInspectable

5
如果你问它是否能工作,很可能可以。 这样正确吗?
  1. 您应该使用 in.c_str() 而不是 &in[0]
  2. 您至少应该在第一次检查 MultiByteToWideChar 的返回值。
  3. MultiByteToWideChar 使用 (-1) 长度进行调用,如果成功,则会计入零终止符(即它将始终返回>= 1 的成功)。 对于std :: wstring的长度构造函数不需要这个。 std::wstring(5,0) 将为六个宽字符分配空间; 5+zero-term。 因此,技术上,您正在分配一个多余的宽字符。

来自MultiByteToWideChar文件关于cbMultiByte和-1的说明:

如果此参数为 -1,则函数处理整个输入字符串,包括终止空字符。因此,生成的 Unicode 字符串具有终止空字符,并且函数返回的长度包括此字符。


in.c_str()不起作用,必须使用&in[0]。此外,我通常传递in.size()而不是-1,这会导致它不附加空值,这意味着代码的其余部分不必更改并变得正确。 - Mooing Duck
@Mooing:我不明白为什么 in.c_str() 不行,而 &in[0] 可以。两者都返回一个 const char*,前者是零终止的。你能解释一下吗? - IInspectable
2
@MooingDuck非常好的一点是传递输入字符串长度而不是-1。这并不改变我的评论,关于当(-1) *被传递时发生的1-wchar_t超量,但你绝对正确,应该使用in.length()。现在我要检查我工作代码中所有这样做的地方...谢谢=P - WhozCraig
@WhozCraig 关于为什么 in.c_str() 不起作用,它适用于 LPCSTR 参数,但 LPWSTR 参数不接受 const char * - NateS

0
其他答案都很好,但我想根据自己对相同问题的研究添加一些额外的信息,以供未来访问者参考。
  • 微软开发者Larry Osterman写了一篇很好的博客文章,描述了这样一个函数,并提出了关于返回代码检查和NRVO(命名返回值优化)的非常好的观点。如果这篇文章仍然可用,你应该阅读它进行讨论。我在这里包含他的最终代码,以防这篇文章丢失。

    
    std::wstring UnicodeStringFromAnsiString(_In_ const std::string &ansiString)
    {
        std::wstring returnValue;
        auto wideCharSize = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, ansiString.c_str(), -1, nullptr, 0);
        if (wideCharSize == 0)
        {
            return returnValue;
        }
        returnValue.resize(wideCharSize);
        wideCharSize = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, ansiString.c_str(), -1, &returnValue[0], wideCharSize);
        if (wideCharSize == 0)
        {
            returnValue.resize(0);
            return returnValue;
        }
        returnValue.resize(wideCharSize-1);
        return returnValue;
    }
    

  • 在我的使用中,我能够添加博客评论中提到的优化,并且不需要对 ANSI 字符串长度使用 -1。
  • C++17(第21.3.1.7.1节)文档中新增了一个非常量data()方法,应该使用它来获取可变指针而不是&in[0]

    charT* data() noexcept;

  • STL在c_str()结果中拥有尾随的\0,因此请注意如何操作字符串大小。


  • 1
    坦白说,这绝对是我第一次必须不同意Larry Osterman的观点。代码有三个问题:1它分配了一个多余的字符(只在最后一步调整大小)。2它没有考虑嵌入的NUL字符(std::string允许这些字符存在)。3它不能正确报告错误(你真的要检查返回的字符串是否为空且源字符串不为空吗?)。完全不清楚为什么代码不使用输入字符串的长度。 - IInspectable
    好的,4 实现检查了第二次调用 MultiByteToWideChar 的返回值,尽管它知道它不可能失败。它在相同的输入上操作,并确保输出是有效的。 - IInspectable

    0
    不可以,因为一个 std::wstring 不能保证将其数据存储在连续的内存块中(尽管在您的实现中很可能会这样)。请改用 std::vector<wchar_t>

    3
    std::wstring 保证以一段连续的内存块存储字符序列。虽然标准没有明确说明,但由于 c_str() 必须在常数时间内返回,因此除了使用连续的内存块,没有其他方法来存储该序列。 - IInspectable
    3
    在当前的标准中,连续存储被明确阐述。§21.4.1/5: "basic_string 对象中的 char-like 对象必须被连续存储。" - Jerry Coffin
    @Jerry:感谢提供的信息。我想我的基于1998年的知识需要进行全面更新了。 - IInspectable
    1
    @Tim:嗯,但即使c_str()的复杂度意味着字符序列存储在连续块中,您仍然无法暗示&s[0]会产生指向此块开头的指针,对吧?或者有没有某个规律表明s.c_str() + i必须产生与&s[i]相同的地址? - Frerich Raabe
    @JerryCoffin:啊,这是个好消息。我得检查一下我的代码,看看有没有使用std::vector将数据复制到std::string中的地方。 :-) - Frerich Raabe
    @Frerich: §21.4.7.1/1: const charT* c_str() const noexcept; _"返回:一个指针p,使得对于每个i在[0,size()]中,p + i == &operator"_(我终于来到了这个千年,这是来自ISO / IEC 14882:2011) - IInspectable

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