Skip to content
C++
Library
since C++98
Basic

Wide Strings

Character and string types for wide character sets, including wchar_t, std::wstring, and Unicode-aware character types for international text.

Wide Stringssince C++98

Strings using wide character types (wchar_t or Unicode-aware char8_t, char16_t, char32_t) to represent character sets that don't fit in a single byte, particularly for international scripts and Unicode text.

Overview

Wide strings provide support for character sets larger than ASCII, such as Japanese kanji, Cyrillic, and other international scripts. The fundamental wide character type is wchar_t, an implementation-defined integral type typically 2–4 bytes. The Standard Library defines std::wstring as a specialization of std::basic_string<wchar_t>.

Since C++11, character types with explicit Unicode encoding semantics have been added to address wchar_t's portability limitations:

  • char8_t (C++20): UTF-8; std::u8string
  • char16_t (C++11): UTF-16; std::u16string
  • char32_t (C++11): UTF-32; std::u32string

These types have guaranteed sizes across all platforms and clearly specify their encoding, making them preferred for portable Unicode handling. Legacy wchar_t varies: Windows typically uses 2 bytes (UTF-16); POSIX systems often use 4 bytes (UTF-32), leading to portability issues when storing Unicode characters outside the basic multilingual plane.

The practical rule of thumb: use UTF-8 (std::u8string or plain std::string with a UTF-8 convention) for storage, files, and network I/O; use std::wstring only at OS API call sites on Windows; use std::u32string when you need to iterate or index by Unicode code point in a portable way.


Syntax

Character and String Literals

Wide character literals and wide string literals use the L prefix:

cpp
wchar_t ch = L'Ω';           // wide character literal
std::wstring str = L"Москва"; // wide string literal

C++11 and later introduce prefixes for explicit Unicode encoding:

cpp
auto u8str  = u8"hello";   // C++11-17: const char*; C++20: const char8_t*
auto u16str = u"Москва";   // const char16_t* — UTF-16 // C++11
auto u32str = U"hello";    // const char32_t* — UTF-32 // C++11

Note the u8"" type change between C++17 and C++20 — see the char8_t migration section below.

String Type Definitions

cpp
#include <string>

std::wstring ws;                          // wchar_t-based string
std::wstring ws2 = L"wide string";
std::u8string u8s = u8"UTF-8";           // C++20: char8_t; prior: char
std::u16string u16s = u"UTF-16";         // char16_t // C++11
std::u32string u32s = U"UTF-32";         // char32_t // C++11

String Views

Since C++17, non-owning view types are available:

cpp
#include <string_view>

std::wstring_view wsv    = L"wide";       // wchar_t    // C++17
std::u16string_view u16sv = u"UTF-16";   // char16_t   // C++17
std::u32string_view u32sv = U"UTF-32";   // char32_t   // C++17
std::u8string_view u8sv   = u8"UTF-8";   // char8_t    // C++20

Examples

Basic Wide String Operations

cpp
#include <string>
#include <iostream>

int main() {
    std::wstring greeting = L"こんにちは"; // Japanese "hello"
    std::wstring name     = L"世界";       // "world"

    std::wstring message = greeting + L" " + name;
    std::wcout << message << std::endl;
}

Numeric Conversions with Wide Strings

std::to_wstring mirrors std::to_string for all arithmetic types. The std::stoi/std::stod family has wide-string overloads that accept std::wstring.

cpp
#include <string>

int value   = 42;
double pi   = 3.14159;

std::wstring wstr_int    = std::to_wstring(value);  // L"42"    // C++11
std::wstring wstr_double = std::to_wstring(pi);     // L"3.141590"

// Parse back
int    n = std::stoi(L"123");       // 123  // C++11
double d = std::stod(L"3.14");      // 3.14

Wide String Streams

cpp
#include <sstream>
#include <iostream>

int main() {
    std::wstringstream wss;
    wss << L"Temperature: " << 25 << L"°C";

    std::wstring result = wss.str();
    std::wcout << result << std::endl;  // wide output
}

UTF-32 for Complete Unicode Coverage

UTF-32 is the only string type in standard C++ where each element is always exactly one complete Unicode code point, making iteration and indexing safe without surrogate-pair arithmetic.

cpp
#include <string>

// Each char32_t holds exactly one code point — no surrogate pairs // C++11
std::u32string emojis = U"🌍🌎🌏";

std::size_t codepoints = emojis.size();  // 3 — correct

for (char32_t ch : emojis) {
    // ch is one complete Unicode scalar value
}

// On Windows, wchar_t is 2 bytes: the same emojis become surrogate pairs
// emojis.size() on wstring would return 6, not 3

String View for Zero-Copy Passing

cpp
#include <string_view>

// Takes any wchar_t sequence with no allocation                  // C++17
void display_title(std::wstring_view sv) {
    for (wchar_t ch : sv) { /* render ch */ }
}

std::wstring title = L"Hello World";
display_title(title);       // implicit conversion, no copy
display_title(L"Literal");  // also works — literals bind directly

Encoding Conversion Between Types

The standard library historically provided std::codecvt facets for encoding conversion, but these were deprecated in C++17 and removed in C++26. The replacements fall into three tiers: the low-level C <cuchar> functions, platform APIs, and third-party libraries.

What Replaced std::codecvt

std::wstring_convert and the std::codecvt facets (std::codecvt_utf8, std::codecvt_utf16) were deprecated in C++17 because their error-handling contract was incomplete and the API was fragile under ill-formed UTF sequences. In C++26 they are removed.

The C++23 standard does not yet ship a blessed replacement in the standard library. For new code, the options are:

NeedSolution
Portable UTF-8 ↔ UTF-32<cuchar> (mbrtoc32 / c32rtomb)
Windows paths / Win32 APIMultiByteToWideChar / WideCharToMultiByte
POSIX systemsiconv
Cross-platform production codeICU or simdutf

<cuchar>: Low-Level C11 Unicode Functions

<cuchar> exposes mbrtoc32, c32rtomb, mbrtoc16, and c16rtomb. These work on multibyte sequences (what the locale calls "multibyte") and explicit UTF-32/UTF-16 values. When the locale is set to a UTF-8 locale, mbrtoc32 converts UTF-8 to UTF-32 code points.

cpp
#include <cuchar>
#include <string>
#include <clocale>
#include <stdexcept>

// UTF-8 std::string  →  std::u32string
// Requires: the locale's multibyte encoding is UTF-8
// (set via setlocale or LC_ALL=en_US.UTF-8 in the environment)
std::u32string utf8_to_utf32(std::string_view utf8) {
    std::u32string result;
    result.reserve(utf8.size());

    std::mbstate_t state{};
    const char* p   = utf8.data();
    const char* end = p + utf8.size();

    while (p < end) {
        char32_t cp;
        std::size_t rc = std::mbrtoc32(&cp, p, end - p, &state);
        if (rc == static_cast<std::size_t>(-1))
            throw std::runtime_error("invalid UTF-8 sequence");
        if (rc == static_cast<std::size_t>(-2))
            throw std::runtime_error("incomplete UTF-8 sequence");
        if (rc == 0) { result.push_back(U'\0'); ++p; continue; }
        result.push_back(cp);
        p += rc;
    }
    return result;
}

// std::u32string  →  UTF-8 std::string
std::string utf32_to_utf8(std::u32string_view u32) {
    std::string result;
    result.reserve(u32.size() * 4);

    std::mbstate_t state{};
    char buf[MB_LEN_MAX];

    for (char32_t cp : u32) {
        std::size_t rc = std::c32rtomb(buf, cp, &state);
        if (rc == static_cast<std::size_t>(-1))
            throw std::runtime_error("invalid code point");
        result.append(buf, rc);
    }
    return result;
}

These functions are low-level and locale-dependent. They work well in programs that set the locale at startup and need no external dependencies.

Platform APIs on Windows

On Windows, std::wstring is UTF-16 (2 bytes per code point in the BMP, surrogate pairs for supplementary characters). All "W" variants of Win32 APIs require LPCWSTR — i.e., a const wchar_t*. The reliable way to convert UTF-8 to std::wstring on Windows is MultiByteToWideChar:

cpp
// Windows-only helper: UTF-8 std::string  →  std::wstring (UTF-16)
#ifdef _WIN32
#include <windows.h>
#include <string>
#include <stdexcept>

std::wstring utf8_to_wstring(std::string_view utf8) {
    if (utf8.empty()) return {};

    int needed = MultiByteToWideChar(
        CP_UTF8, MB_ERR_INVALID_CHARS,
        utf8.data(), static_cast<int>(utf8.size()),
        nullptr, 0);

    if (needed == 0)
        throw std::runtime_error("MultiByteToWideChar failed");

    std::wstring result(needed, L'\0');
    MultiByteToWideChar(
        CP_UTF8, MB_ERR_INVALID_CHARS,
        utf8.data(), static_cast<int>(utf8.size()),
        result.data(), needed);
    return result;
}

// std::wstring (UTF-16)  →  UTF-8 std::string
std::string wstring_to_utf8(std::wstring_view wide) {
    if (wide.empty()) return {};

    int needed = WideCharToMultiByte(
        CP_UTF8, WC_ERR_INVALID_CHARS,
        wide.data(), static_cast<int>(wide.size()),
        nullptr, 0, nullptr, nullptr);

    if (needed == 0)
        throw std::runtime_error("WideCharToMultiByte failed");

    std::string result(needed, '\0');
    WideCharToMultiByte(
        CP_UTF8, WC_ERR_INVALID_CHARS,
        wide.data(), static_cast<int>(wide.size()),
        result.data(), needed, nullptr, nullptr);
    return result;
}
#endif

Pass MB_ERR_INVALID_CHARS and WC_ERR_INVALID_CHARS to get hard errors on ill-formed sequences rather than silent replacement characters.

iconv on POSIX

On Linux and macOS, iconv from <iconv.h> converts between any two named encodings:

cpp
#include <iconv.h>
#include <string>
#include <stdexcept>
#include <cerrno>

// UTF-8 std::string  →  std::u32string via iconv (POSIX)
std::u32string utf8_to_utf32_iconv(const std::string& utf8) {
    iconv_t cd = iconv_open("UTF-32LE", "UTF-8");
    if (cd == reinterpret_cast<iconv_t>(-1))
        throw std::runtime_error("iconv_open failed");

    std::u32string result(utf8.size(), U'\0');

    const char* inbuf  = utf8.data();
    size_t inbytesleft = utf8.size();
    char*  outbuf      = reinterpret_cast<char*>(result.data());
    size_t outbytesleft = result.size() * sizeof(char32_t);

    if (iconv(cd, const_cast<char**>(&inbuf), &inbytesleft,
              &outbuf, &outbytesleft) == static_cast<size_t>(-1)) {
        iconv_close(cd);
        throw std::runtime_error("iconv conversion failed");
    }
    iconv_close(cd);

    // Trim to actual output size
    result.resize((result.size() * sizeof(char32_t) - outbytesleft)
                  / sizeof(char32_t));
    return result;
}

u16string ↔ u32string Without Platform APIs

Converting between UTF-16 and UTF-32 is straightforward since the mapping between code units and code points is defined by the Unicode standard. A portable helper that avoids platform APIs:

cpp
#include <string>
#include <stdexcept>

// std::u32string  →  std::u16string (encode supplementary as surrogate pairs)
std::u16string utf32_to_utf16(std::u32string_view u32) {
    std::u16string result;
    result.reserve(u32.size());

    for (char32_t cp : u32) {
        if (cp < 0xD800 || (cp > 0xDFFF && cp < 0x10000)) {
            result.push_back(static_cast<char16_t>(cp));
        } else if (cp <= 0x10FFFF) {
            // Encode as surrogate pair
            cp -= 0x10000;
            result.push_back(static_cast<char16_t>(0xD800 | (cp >> 10)));
            result.push_back(static_cast<char16_t>(0xDC00 | (cp & 0x3FF)));
        } else {
            throw std::runtime_error("invalid Unicode code point");
        }
    }
    return result;
}

// std::u16string  →  std::u32string (decode surrogate pairs)
std::u32string utf16_to_utf32(std::u16string_view u16) {
    std::u32string result;
    result.reserve(u16.size());

    for (std::size_t i = 0; i < u16.size(); ++i) {
        char16_t unit = u16[i];
        if (unit < 0xD800 || unit > 0xDFFF) {
            result.push_back(static_cast<char32_t>(unit));
        } else if (unit <= 0xDBFF) {
            // High surrogate
            if (i + 1 >= u16.size() || u16[i+1] < 0xDC00 || u16[i+1] > 0xDFFF)
                throw std::runtime_error("unpaired high surrogate");
            char32_t cp = 0x10000
                        + ((static_cast<char32_t>(unit)    & 0x3FF) << 10)
                        + ( static_cast<char32_t>(u16[i+1]) & 0x3FF);
            result.push_back(cp);
            ++i;  // consumed the low surrogate
        } else {
            throw std::runtime_error("unexpected low surrogate");
        }
    }
    return result;
}

Windows API Interop

Windows exposes two API flavors for every text-accepting function: an A (ANSI/narrow) suffix and a W (wide/Unicode) suffix. The W variants accept LPCWSTR (const wchar_t*). When building with UNICODE defined (the default in most modern Windows projects), generic names like CreateFile resolve to CreateFileW.

The L"" Literal Requirement

Win32 wide APIs require wchar_t*, not char16_t*. Even though they are both 2 bytes on Windows, the types are distinct. Always use the L"" prefix for string literals passed to Win32:

cpp
#ifdef _WIN32
#include <windows.h>
#include <string>

// Open a file by wide path — uses CreateFileW under the hood
HANDLE open_file(const std::wstring& path) {
    return CreateFileW(
        path.c_str(),            // LPCWSTR — wchar_t*, not char16_t*
        GENERIC_READ,
        FILE_SHARE_READ,
        nullptr,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        nullptr
    );
}

// Show a message box
void show_message(const std::wstring& msg, const std::wstring& title) {
    MessageBoxW(nullptr, msg.c_str(), title.c_str(), MB_OK | MB_ICONINFORMATION);
}
#endif

UTF-8 ↔ wstring at the API Boundary

A common pattern in cross-platform code is to store all strings internally as UTF-8 (plain std::string) and convert to std::wstring only when calling Win32 APIs:

cpp
#ifdef _WIN32
#include <windows.h>
#include <string>
#include <fstream>

// Internal representation: UTF-8 std::string everywhere.
// At Win32 call sites, convert once.
bool file_exists(const std::string& utf8_path) {
    std::wstring wide_path = utf8_to_wstring(utf8_path);  // from helpers above
    DWORD attrs = GetFileAttributesW(wide_path.c_str());
    return attrs != INVALID_FILE_ATTRIBUTES;
}

// std::filesystem (C++17) handles the conversion internally:
#include <filesystem>
bool file_exists_portable(const std::string& utf8_path) {
    // std::filesystem::path accepts UTF-8 on all platforms  // C++17
    return std::filesystem::exists(
        std::filesystem::path(utf8_path)
    );
}
#endif

std::filesystem::path (C++17) is the recommended way to pass paths on Windows without manual wstring conversion — it stores paths in the native wide format internally and handles the conversion transparently.

Wide Console Output on Windows

The Windows console requires special setup to display Unicode correctly:

cpp
#ifdef _WIN32
#include <windows.h>
#include <io.h>
#include <fcntl.h>
#include <iostream>

void setup_unicode_console() {
    // Switch stdout/stderr to UTF-16 mode on Windows
    _setmode(_fileno(stdout), _O_U16TEXT);
    _setmode(_fileno(stderr), _O_U16TEXT);
    // After this, use std::wcout / std::wcerr, not std::cout
}

int main() {
    setup_unicode_console();
    std::wcout << L"日本語テキスト\n";
}
#endif

Alternatively, set the code page to UTF-8 with SetConsoleOutputCP(CP_UTF8) and use std::cout with UTF-8 encoded strings.


C++20 char8_t Migration

C++20 changed the type of u8"" string literals from const char* to const char8_t*. This is a source-breaking change — code that compiled under C++17 may not compile under C++20 without adjustment.

Code That Breaks

cpp
// Compiled fine with -std=c++17, fails with -std=c++20:
const char* p = u8"hello";             // C++17: OK; C++20: error
std::string s = u8"こんにちは";        // C++17: OK; C++20: error
void func(const char*);
func(u8"test");                        // C++17: OK; C++20: error

The Fixes

Option 1 — Drop the u8 prefix when you just want a UTF-8 std::string (valid since source files are typically UTF-8):

cpp
std::string s = "こんにちは";   // relies on source encoding being UTF-8

Option 2 — Use std::u8string explicitly (C++20):

cpp
std::u8string s = u8"こんにちは";   // correct in C++20

Option 3 — Keep u8"" and cast for C API interop:

cpp
// C++20: u8"..." is const char8_t*, cast to const char* for C APIs
const char8_t* u8lit = u8"hello";
const char* c_str = reinterpret_cast<const char*>(u8lit);  // C++20

// Or wrap in a helper:
constexpr const char* as_char(const char8_t* p) noexcept {
    return reinterpret_cast<const char*>(p);
}

void legacy_c_api(const char*);
legacy_c_api(as_char(u8"hello"));  // C++20

The reinterpret_cast from char8_t* to char* is well-defined because char8_t has the same underlying representation as unsigned char, and reading a char8_t buffer through const char* is permitted by the aliasing rules in [basic.lval].

Detecting the Type in Shared Code

If you need a single codebase to work across C++17 and C++20, use a macro:

cpp
#if __cpp_char8_t >= 201811L  // C++20 feature test macro
    using utf8char = char8_t;
    using utf8string = std::u8string;
    #define U8(x) u8##x
#else
    using utf8char = char;
    using utf8string = std::string;
    #define U8(x) x
#endif

utf8string greeting = U8("hello");

std::u8string and std::string Are Distinct Types

Even though both hold bytes, std::u8string and std::string are not implicitly convertible. Bridges require explicit construction:

cpp
std::u8string u8s = u8"test";   // C++20

// std::u8string  →  std::string  (bit-identical, just different types)
std::string s(reinterpret_cast<const char*>(u8s.data()), u8s.size());

// std::string  →  std::u8string
std::u8string u8s2(reinterpret_cast<const char8_t*>(s.data()), s.size());

This is verbose intentionally — the type system flags all places where encoding is being assumed.


Performance Considerations

Memory Footprint

The encoding choice has a direct effect on memory:

TypeBytes per BMP characterBytes per supplementary char
std::string (UTF-8)1–34
std::wstring (Windows, UTF-16)24 (surrogate pair)
std::wstring (Linux, UTF-32)44
std::u16string24 (surrogate pair)
std::u32string44

For text that is predominantly ASCII (English prose, source code, JSON keys, URLs), UTF-8 uses roughly one-quarter the memory of std::u32string. For CJK text (every character is 3 bytes in UTF-8 vs. 2 bytes in UTF-16), UTF-16 is slightly more compact.

Processing Speed

UTF-8 comparisons and searches can be faster in practice because:

  • Smaller memory footprint improves cache utilization.
  • Hardware prefetchers and SIMD string instructions (SSE4.2, AVX-512) target 8-bit lanes natively.
  • Most real-world data is ASCII-heavy, where UTF-8 values are identical to plain ASCII byte values.

Wide strings are slower for the inverse reasons: more bytes in cache, wider load instructions required, and operations that appear O(n) in code units may be O(n/2) or O(n/4) in actual Unicode characters (confusing complexity analysis).

The Architecture Rule

Keep wide strings out of your data model. The canonical architecture:

cpp
[Storage / Network / Files]   →  UTF-8 (std::string or std::u8string)
        ↓ one conversion point ↓
[OS API boundary (Windows)]   →  std::wstring (convert once per call)
        ↑ one conversion point ↑
[Application logic]           →  UTF-8 (std::string or std::u8string)

Converting at the boundary keeps the expensive wide-string processing confined to one place and means the rest of the codebase is portable across platforms.

std::u32string for Algorithm Correctness

When code logic depends on character count (centering a string, truncating at N characters), UTF-8 and UTF-16 require walking the string to count code points rather than reading .size(). std::u32string trades memory for algorithmic simplicity:

cpp
// UTF-8: counting code points requires iteration
std::size_t count_codepoints_utf8(std::string_view s) {
    std::size_t n = 0;
    for (unsigned char c : s)
        if ((c & 0xC0) != 0x80) ++n;  // count non-continuation bytes
    return n;
}

// UTF-32: direct — one element is always one code point  // C++11
std::size_t count_codepoints_utf32(std::u32string_view s) {
    return s.size();  // O(1), always correct
}

If your algorithm slices, pads, or counts by Unicode character (not byte), std::u32string eliminates an entire class of bugs at the cost of 4× memory.


Best Practices

Prefer UTF-8 internally; convert to std::wstring only at Win32 call sites.
Store text as std::string (UTF-8 convention) throughout your application. Convert to std::wstring once, immediately before the Win32 call, and convert back immediately after. This keeps all the encoding complexity in one place and makes the code portable to non-Windows platforms without changes.

Use std::filesystem::path instead of manual wstring construction for file paths (C++17).
std::filesystem::path accepts UTF-8 on all platforms and converts to the native encoding internally. It eliminates the need to write utf8_to_wstring() helpers for path operations.

cpp
#include <filesystem>
// Works on Windows (converts to UTF-16 internally) and POSIX
auto p = std::filesystem::path("data/файл.txt");  // C++17

Pass wide strings as views, not by value or const reference (C++17).
std::wstring_view, std::u32string_view, etc. accept both std::wstring arguments and string literals without a copy.

cpp
// Prefer
void render_label(std::wstring_view text);   // C++17

// Avoid
void render_label(const std::wstring& text); // forces a heap allocation from literals

Use std::u32string for code that indexes or slices by character, not byte.
Indexing a std::u16string or std::wstring (Windows) by position gives you a code unit, not necessarily a code point. std::u32string is the only standard type where s[i] reliably gives you one complete Unicode character.

Replace std::codecvt (deprecated C++17, removed C++26) with explicit conversion helpers.
Do not write new code using std::wstring_convert or std::codecvt_utf8. Write a small helper using MultiByteToWideChar/WideCharToMultiByte on Windows, iconv on POSIX, or a library like simdutf. Put the helpers in one translation unit and call them from everywhere.

Fix u8"" literal usage when moving to C++20.
Search for const char* p = u8"..." patterns before enabling C++20. Use reinterpret_cast<const char*> as the interop bridge where C APIs require const char*, and migrate application-internal strings to std::u8string where encoding semantics matter.


Common Pitfalls

Assuming wchar_t portability.
The size and encoding of wchar_t are implementation-defined. Code that works on Windows (2 bytes, UTF-16) may fail on Linux (4 bytes, UTF-32). A std::wstring containing the emoji U+1F30D is two wchar_t elements on Windows and one on Linux. Use std::u32string for portable algorithms that reason about code points.

Silent character loss when widening without conversion.
Constructing std::wstring from a narrow std::string using the range constructor std::wstring(s.begin(), s.end()) widens each char individually — this only works for ASCII. Any byte above 0x7F is sign-extended or zero-extended to wchar_t rather than decoded as UTF-8. Use an explicit conversion function.

cpp
std::string utf8 = "café";
// Wrong: treats each byte as a separate character
std::wstring bad(utf8.begin(), utf8.end());

// Correct: use proper conversion (platform or library)
std::wstring good = utf8_to_wstring(utf8);

Surrogate pair handling.
On Windows, wchar_t is 2 bytes. Characters outside the BMP (U+10000 and above — emojis, rare scripts, mathematical symbols) are encoded as two wchar_t values called a surrogate pair. std::wstring::size() returns the code unit count, not the character count. Iterating by index and operating on individual wchar_t values can silently split a surrogate pair.

The u8"" type break in C++20.
Enabling -std=c++20 (or std:c++20 in MSVC) makes u8"..." literals return const char8_t*, breaking assignments to const char* or std::string. Audit every u8"" literal before upgrading.

Using std::codecvt in new code.
std::codecvt_utf8, std::codecvt_utf16, and std::wstring_convert compile cleanly through C++23 but emit deprecation warnings and are removed in C++26. Do not use them in new code; migrate existing code before upgrading to C++26.

Incomplete locale support for character classification.
iswalpha(), iswupper(), and related functions are locale-dependent. The same character may classify differently on different operating systems with different locale data. For deterministic results, use the Unicode property tables in ICU or a similar library.


See Also