Форматирование кириллицы в C++ при выводе в терминал через cout

В книге Р.Лафоре "ООП в С++" есть пример кода, в котором присутствует кириллица:

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    long pop1 = 8425785, pop2 = 47, pop3 = 9761;
    cout << setw(9) << "Город" << setw(12) << "Население" << endl
            << setw(9) << "Москва" << setw(12) << pop1 << endl
            << setw(9) << "Киров" << setw(12) << pop2 << endl
            << setw(9) << "Угрюмовка" << setw(12) << pop3 << endl;
    return 0;
}

Суть задачи заключается в выводе и форматировании текста в виде таблицы. Так как пример написан под использование IDE MVS - то проблем там с выводом и форматированием не должно возникнуть (я так думаю, раз в книге указано).

Решение задачи

Проблема в том, что форматирование кириллицы не получается в среде Linux. Так как кириллица - это уже многобайтовая кодировка, то не удаётся корректно отформатировать текст с использованием setw(). Есть ли какой пример или решение данного вопроса - форматирования кириллицы?

Мой результат в терминале:

Результат в терминале


Ответы (2 шт):

Автор решения: Fat-Zer

К сожалению, простого переносимого способа сделать это нет.

Единственный маломальски-адекватный вариант — это использовать широкие символы:

int main() {
  setlocale(LC_ALL, ""); // Для корректной работы необходимо инициировать локаль

  wcout << setw(9) << L"Город" << setw(12) << L"Население" << endl;
  // ...
}

Но, это несёт с собой отдельный ворох проблем:

  • Широкие символы придётся использовать практически везде.
  • Нормального переносимого способа конвертировать широкие символы в обычные в C++ нет до сих пор.
  • Все системные вызовы в *nix используют обычные строки.
→ Ссылка
Автор решения: Stanislav Volodarskiy

Обходный маневр! UTF-8 устроен так что легко можно вычислить число символов (не байт) в строке. Смотрим на два старших бита символа:

int utf8len(const char *s) {
    int len = 0;
    for (; *s != '\0'; ++s) {
        len += (*s & 0xC0) != 0x80;
    }
    return len;
}

Зная длину строки в символах пишем псевдоманипулятор. "Псевдо" потому что он работает правильно только в одном единственном случае. А именно, когда этот манипулятор попадает в операцию <<, он возвращает прокси объект, не поток. Этот прокси объект тоже умеет делать << и возвращает поток восстанавливая нормальное положение дел:

... << utf8setw(8) << "Город" << ...
    ^              ^          ^
    |              |          обрабатывает ostream
    |              обрабатывает Utf8SetWProxy
    обрабатывает ostream

Ограничений много: новый манипулятор не работает с другими стандартными манипуляторами, операции вывода нельзя прервать между манипулятором и строкой. Зато выглядит совсем как настоящий.

// манипулятор
// печатает выровненную вправо строку
class utf8setw {
public:
    utf8setw(int w) : w(w) {}
    void print(ostream &os, const char *s) const {
        int pad = w - utf8len(s);
        for (int i = 0; i < pad; ++i) {
            os << ' ';
        }
        os << s;
    }
private:
    int w;
};

// прокси
// сохраняет манипулятор, поток вывода и ждёт следующий аргумент-строку
// когда строка приходит, передаёт её и поток манипулятору
class Utf8SetWProxy {
public:
    Utf8SetWProxy(ostream &os, const utf8setw &manip) : os(os), manip(manip) {}
    friend ostream &operator <<(const Utf8SetWProxy &p, const char *s) {
        p.manip.print(p.os, s);
        return p.os;
    }
private:
    ostream &os;
    const utf8setw &manip;
};

// трюк, который запускает процесс
// обратите внимание на "неправильный" тип результата
// следующий оператор `<<` будет обрабатываться не потоком вывода    
Utf8SetWProxy operator <<(ostream &os, const utf8setw &manip) {
    return Utf8SetWProxy(os, manip);
}

Пример использования. Сперва печатается стандартный вариант, который не выравнивает кириллические строки, так как подразумевает, что число байтов и число символов в строке одно и то же. Затем новый вариант. Код выглядит почти так же ...

int main()
{
    long pop1 = 8425785, pop2 = 47, pop3 = 9761;
    cout << setw(9) << "Город" << setw(12) << "Население" << endl
         << setw(9) << "Москва" << setw(12) << pop1 << endl
         << setw(9) << "Киров" << setw(12) << pop2 << endl
         << setw(9) << "Угрюмовка" << setw(12) << pop3 << endl;

    cout << endl;

    cout << utf8setw(9) << "Город" << utf8setw(12) << "Население" << endl
         << utf8setw(9) << "Москва" << setw(12) << pop1 << endl
         << utf8setw(9) << "Киров" << setw(12) << pop2 << endl
         << utf8setw(9) << "Угрюмовка" << setw(12) << pop3 << endl;
    return 0;
}

... но выравнивает строки куда лучше:

ГородНаселение
Москва     8425785
Киров          47
Угрюмовка        9761

    Город   Население
   Москва     8425785
    Киров          47
Угрюмовка        9761
→ Ссылка