Бен Ки

31 октября 2013 г.; 09 ноября 2018 г.

В этой статье описывается межплатформенный метод преобразования между STL string и STL wstring. Метод, описанный в этой статье, не использует никаких внешних библиотек. Он также не использует какие-либо специфичные для операционной системы API. Он использует только функции, которые являются частью стандартной библиотеки шаблонов.

описание проблемы

Стандартная библиотека шаблонов предоставляет класс шаблонов basic_string для представления последовательности символов. Он поддерживает все обычные операции с последовательностями и стандартные string операции, такие как поиск и объединение.

Есть две общие специализации шаблонного класса basic_string. Это string, то есть typedef вместо basic_string<char>, и wstring, то есть typedef вместо basic_string<wchar_t>. Кроме того, есть две специализации шаблонного класса basic_string, которые являются новыми для C++11. Новыми специализациями шаблонного класса basic_string являются u16string, который является typedef для basic_string<char16_t>, и u32string, который является typedef для basic_string<char32_t>.

В Интернете часто задают вопрос, как преобразовать string в wstring. Это может быть необходимо при вызове функции API, предназначенной для одного типа строки, из кода, использующего другой тип строки. Например, ваш код может использовать объекты wstring, и вам может потребоваться вызвать функцию API, которая использует объекты string. В этом случае вам нужно будет преобразовать wstring в string перед вызовом функции API.

К сожалению, стандартная библиотека шаблонов не предоставляет простых средств для такого преобразования. В результате, это часто задаваемый вопрос в Интернете. Поиск в Google по запросу конвертировать между строкой и wstring дает около 76 300 результатов. К сожалению, многие из предоставленных ответов неверны.

Неправильный ответ

Самый распространенный неверный ответ, найденный в Интернете, можно найти в статье Конвертировать std::string в std::wstring на Mijalko.com. Решение, представленное в этой статье, выглядит следующим образом.

// std::string -> std::wstring
std::string s("string");
std::wstring ws;
ws.assign(s.begin(), s.end());
// std::wstring -> std::string
std::wstring ws(L"wstring");
std::string s;
s.assign(ws.begin(), ws.end());

Проблема в том, что это решение компилируется и запускается, а простые строковые константы, используемые в этом примере кода, дают правильные результаты. Многие нестандартные программисты предполагают, что, поскольку это работает для этих очень простых строк, они нашли правильное решение. Затем они переводят свое приложение на другие языки, помимо английского, и очень удивляются, обнаружив, что их приложение изобилует ошибками, которые затрагивают только их неанглоязычных клиентов!

Проблема в том, что это решение правильно работает только для символов ASCII в диапазоне от 0 до 127. Если ваши строки содержат хотя бы один символ с числовым значением больше 127, это простое решение даст неверные результаты. Другими словами, это простое решение приведет к неправильным результатам, если ваши строки содержат какие-либо символы, кроме символов от a до z, от A до Z, цифр от 0 до 9 и нескольких знаков препинания.

Это означает, что любое приложение, использующее этот метод, не будет поддерживать китайский язык. Он даже не будет должным образом поддерживать испанский язык, поскольку испанский алфавит содержит несколько символов, не входящих в набор символов ASCII, таких как ch (ce hache), ll (elle) и ñ (eñe).

Правильное решение

В этой статье описывается кроссплатформенное решение этой проблемы со следующими характеристиками.

Функции

namespace yekneb
{
template<typename Target, typename Source>
inline Target string_cast(const Source& source)
{
    return source;
}
template<>
inline std::wstring string_cast(const std::string& source)
{
    std::locale loc;
    return ::yekneb::detail::string_cast::s2w(source, loc);
}
template<>
inline std::string string_cast(const std::wstring& source)
{
    std::locale loc;
    return ::yekneb::detail::string_cast::w2s(source, loc);
}
}

Предусмотрены три дополнительные функции, которые позволяют пользователю указать параметр локали.

Функции используются следующим образом.

std::wstring wIn(L"Hello World! It is truly a wonderful day to be alive.");
std::string sIn("Hello World! It is truly a wonderful day to be alive.");
std::string sOut = yekneb::string_cast<std::string>(wIn);
std::wstring wOut = yekneb::string_cast<std::wstring>(sIn);

Детали

Функции s2w и w2s реализованы с использованием следующих возможностей STL.

Реализация этих функций следующая.

namespace yekneb
{
namespace detail
{
namespace string_cast
{
inline std::string w2s(const std::wstring& ws, const std::locale& loc)
{
    typedef std::codecvt<wchar_t, char, std::mbstate_t>
      converter_type;
    typedef std::ctype<wchar_t> wchar_facet;
    std::string return_value;
    if (ws.empty())
    {
        return "";
    }
    const wchar_t* from = ws.c_str();
    size_t len = ws.length();
    size_t converterMaxLength = 6;
    size_t vectorSize = ((len + 6) * converterMaxLength);
    if (std::has_facet<converter_type>(loc))
    {
        const converter_type& converter =
          std::use_facet<converter_type>(loc);
        if (converter.always_noconv())
        {
            converterMaxLength = converter.max_length();
            if (converterMaxLength != 6)
            {
                vectorSize = ((len + 6) * converterMaxLength);
            }
            std::mbstate_t state;
            const wchar_t* from_next = nullptr;
            std::vector<char> to(vectorSize, 0);
            std::vector<char>::pointer toPtr = to.data();
            std::vector<char>::pointer to_next = nullptr;
            const converter_type::result result = converter.out(
                state, from, from + len, from_next,
                toPtr, toPtr + vectorSize, to_next);
            if (
              (
                converter_type::ok == result
                || converter_type::noconv == result
                )
              && 0 != toPtr[0]
              )
            {
              return_value.assign(toPtr, to_next);
            }
        }
    }
    if (return_value.empty() && std::has_facet<wchar_facet>(loc))
    {
        std::vector<char> to(vectorSize, 0);
        std::vector<char>::pointer toPtr = to.data();
        const wchar_facet& facet = std::use_facet<wchar_facet>(loc);
        if (facet.narrow(from, from + len, '?', toPtr) != 0)
        {
            return_value = toPtr;
        }
    }
    return return_value;
}
inline std::wstring s2w(const std::string& s, const std::locale& loc)
{
    typedef std::ctype<wchar_t> wchar_facet;
    std::wstring return_value;
    if (s.empty())
    {
        return L"";
    }
    if (std::has_facet<wchar_facet>(loc))
    {
        std::vector<wchar_t> to(s.size() + 2, 0);
        std::vector<wchar_t>::pointer toPtr = to.data();
        const wchar_facet& facet = std::use_facet<wchar_facet>(loc);
        if (facet.widen(s.c_str(), s.c_str() + s.size(), toPtr)
          != 0)
        {
            return_value = to.data();
        }
    }
    return return_value;
}
}
}
}

Специфическая ошибка GNU/Linux

Вышеуказанные функции хорошо работают в Microsoft Windows и MAC OS X. Однако они не работают в GNU/Linux. В частности, функция w2s завершится ошибкой с одной из двух ошибок, в зависимости от того, включена ли отладка.

Если отладка включена, ошибка будет двойной ошибкой освобождения или повреждения, возникающей при освобождении выходного буфера. Ошибка возникает из-за того, что вызов convert.out приведет к повреждению выходного буфера, и в выходной буфер будет записано гораздо больше байтов, чем количество байтов, выделенных для буфера, даже если вы перераспределите выходной буфер в степени 2.

Если отладка не включена, ошибка будет следующей.

../iconv/loop.c:448: internal_utf8_loop_single: Assertion `inptr - bytebuf > (state->__count & 7)' failed.

Эти ошибки возникают только тогда, когда активная локаль использует UTF-8.

Мне не удалось решить эту проблему, используя только STL. Однако библиотеки Boost C++, найденные на www.boost.org, предоставляют приемлемое решение — функцию boost::locale::conv::utf_to_utf. Если вы используете Boost, эту проблему можно решить следующим образом.

Сначала добавьте функцию IsUTF8 в пространство имен ::yekneb::detail::string_cast. Эта функция может использоваться для определения того, использует ли данная локаль UTF-8 и, следовательно, должна ли функция w2s использовать функцию boost::locale::conv::utf_to_utf.

Исходный код функции IsUTF8 выглядит следующим образом.

inline bool IsUTF8(const std::locale &loc)
{
    std::string locName = loc.name();
    if (
      !locName.empty()
      && std::string::npos != locName.find("UTF-8")
      )
    {
        return true;
    }
    return false;
}

Затем просто добавьте следующий код в функцию w2s сразу после блока кода if (ws.empty()).

if (IsUTF8(loc))
{
    return_value = boost::locale::conv::utf_to_utf<char>(ws);
    if (! return_value.empty())
    {
        return return_value;
    }
}

Для согласованности аналогичный блок кода следует добавить и в функцию s2w.

Исходный код статьи

Полный исходный код этой статьи можно найти на сайте SullivanAndKey.com в заголовочном файле StringCast.h. Вы также можете увидеть код в действии на ideone.com.

Первоначально опубликовано на yekneb.com 10 ноября 2018 г.