UTF-8 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、ポーカープログラムの締めくくりとして、トランプのマークを絵文字で表現できるようにしてみます。そのために、UTF-8 という文字エンコーディング形式を使います。Windows および Visual Studio を使った環境ではいくつか理解しなければならないことがあるので、その辺りの話を中心にして、UTF-8 そのものについては(まじめにやると難しいので)あまり深入りせずに進めます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



Unicode

ポーカープログラムの締めくくりとして、トランプのマークを「♠♣♦♥」のような絵文字で出力できるようにしてみます。

実際の見た目は使っているフォントによって異なります。また、フォントによっては表示できないかもしれません。

トランプで使うこれらのマークは、ASCII や Shift_JIS (CP932) の文字集合には含まれていません(「マルチバイト文字」のページを参照)。そこで、Unicode と呼ばれる文字コード体系を使うようにします。Unicode は、エンコーディング形式と文字集合をそれぞれ定めた規格になっています。Unicode のエンコーディング形式にはいくつか種類があり、このページのテーマである UTF-8 はそのうちの1つです。このほか UTF-16UTF-32 が有名どころです。

Unicode の文字集合には世界中のさまざまな文字が含まれており、たびたび改訂されて新しい文字が増え続けています。この中にトランプのマークも含まれています。Unicode の文字集合内の文字を区別するために、文字集合内で固有の整数表現があり、コードポイント (code point) と呼ばれています。Unicode のコードポイントによる表現と、UTF-8 などのエンコーディング形式による表現は異なるものです。

UTF-8 は1つの文字を 8ビット(1バイト)単位の大きさで表現します。最低で 8ビット(1バイト)、最大で 32ビット(4バイト)です。ASCII で表現できる文字ならば、UTF-8 で表現しても ASCII で表現しても、8ビットに収まる同じ整数になります。

UTF-8 には BOM(バイト順マーク) (Byte Order Mark) という概念があります。BOM は、ファイルの先頭に付加される、ある決まった並びの 3バイトのマークのことです。BOM があることによって、以降のデータが UTF-8 で表現されることをソフトウェアが認識しやすくなります。ただ、BOM は必須ではないため、BOM を認識しないソフトウェアもあり、その場合は BOM があると正常に読めないことがあります(BOM の部分もデータの一部であるように扱ってしまう)。そのため、UTF-8 を使うなら、BOM の有無に関する違いがあることを理解しておかなければなりません。BOM がある UTF-8 を「BOM付きの UTF-8」などと表現し、BOM がない UTF-8 を「BOMなしの UTF-8」とか「UTF-8N」などと表現します。

UTF-8 の BOM は 16進数表記で 0xEF 0xBB 0xBF です。

ソースファイルのエンコーディング形式

C++ のプログラムで UTF-8 を扱うのなら、ソースファイル上に UTF-8 の文字を書けなければ困る場合が多いでしょう。そこでまず、ソースファイル自体のエンコーディング形式を UTF-8 にします。大抵のテキストエディタには、エンコーディング形式を指定する機能があります。

【上級】現在のエンコーディング形式で表記できない文字を、ユニバーサル文字名という仕組みを使って表記する方法もあります。\uNNNN\UNNNNNNNNN という表記方法で、N のところに ISO/IEC 10646(Unicode とほぼ同じもの)のコードポイントを記述すると、該当する文字に置き換えられます。1

Visual Studio では、ソースファイルのエンコーディング形式を次の順序で判断します2

  1. ソースファイルに明確なマークがあれば、それに従う
  2. コンパイラオプションによる指定があれば、それに従う
  3. 現在のコードページの設定に従う

特に何もせずソースファイルを作成していたら、3番のところまで進んで、CP932(Shift_JIS)になっているでしょう。

Windows を除くほとんどの現代の環境では、UTF-8 というエンコーディング形式を使っているため、Windows も UTF-8 がデフォルトになるように変わる可能性があります。今のところのデフォルト設定は CP932 ですが、UTF-8 に変更することは可能になっています。Windows 10 の設定画面より、【時刻と言語】>【言語】>【管理用の言語の設定】と進み、【管理】タブの【システムロケールの変更】を選択。【ベータ: ワールドワイド言語サポートで Unicode UTF-8 を使用】にチェックを入れると、UTF-8 に変更できます。ただしあくまでベータ版扱いの機能ですし、Windows上で動作するすべてのプログラムに影響を与える設定なので、過去に作られたプログラムが誤動作する可能性もあります。

新C++編としては、確実に UTF-8 と判断させるために、ソースファイルのエンコーディング形式を UTF-8 (BOM付き) にしておくことにします。BOM は、上記の順序のうちの1番のところにある「明確なマーク」に該当します。

BOM があって都合が悪い環境では、UTF-8N を使ってください。

Visual Studio で、ソースファイルを UTF-8 (BOM付き) で保存するには、設定を変えておくか、適当なテキストエディタを使って UTF-8 (BOM付き) にしておきます。Visual Studio での設定方法については、「Visual Studio編>ソースファイルのエンコーディング形式」を参照してください。

文字列リテラル、文字リテラルのエンコーディング形式

'あ' とか "あいうえお" のように記述した文字リテラル、文字列リテラルのエンコーディング形式は処理系定義です。Visual Studio の場合は、実行文字集合 (execution character set) に従うことになっています3が、これはソースファイルのエンコーディング形式を決めるときと同じ手順で判断されるので、おそらく CP932 になるでしょう。

リテラルはソースファイル内に記述するものなので、ソースファイルのエンコーディング形式と同じであるようにも思えますが、実際には独立して考えることになります。Visual Studio では、ソースファイルを UTF-8 (BOM付き) にしても、文字リテラルや文字列リテラルが UTF-8 になるわけではありません。

文字リテラルや文字列リテラルの直前に、以下の表に挙げるプリフィックス(接頭辞) (prefix) を付加することによって、エンコーディング形式を強制します。

プリフィックス エンコーディング形式
u8 UTF-8 (※C++14 では文字リテラルのみ) char (※C++20 から char8_t)
L 処理系定義のワイド文字エンコーディング wchar_t
u 処理系定義(多くの場合 UTF-16) char16_t
U 処理系定義(多くの場合 UTF-32) char32_t
なし 処理系定義 char

「型」は、1文字を表現するために用いる型です。

【C++20】uプリフィックスのエンコーディング形式は UTF-16、Uプリフィックスのエンコーディング形式は UTF-32 であることが規定されました。4

【C++20】u8プリフィックスを付加した場合の型は char8_t に変更されました。また、char8_t型による文字列を扱う std::u8string も追加されています。5。また、std::cout の << などに char8_t型の文字や文字列を渡せないため15std::cout << u8"abc"; のようなコードは、C++17 までは受け付けますが(このバージョンまでは char型なので)、C++20 からはコンパイルエラーになります。この問題については、このページの終わりで取り上げます

プリフィックスとは、先頭に付け足すことで何かしらの意味を持つ表記のことです。たとえば、u8プリフィックスは u8"あいうえお" のように用い、こう記述すると、UTF-8 による “あいうえお” であると認識されます。C++14 では、文字リテラルに u8プリフィックスを付加することはできません。

【C++17】文字リテラルに対して u8プリフィックスを付加することが可能になりました。6

今のところ、使うのは u8 だけです。L、u、U もさまざまな文字を取り扱う方法として選択肢に上げられるものではありますが、型が異なっており、文字の扱い方そのものに違いがあるので、当面は取り扱わないことにします。

今後、新C++編では原則として、文字列リテラルにプリフィックスを付加しませんが、Unicode でなければ表現できない文字を使いたいときには u8プリフィックスを付加します。


u8プリフィックスを付加しても文字の型は char型のままです(C++20 で変更されましたが、以下の話題には違いはありません)。char型の大きさは 1バイトですが、UTF-8 では 1文字を 1~4バイトで表現するため、1つの char型の変数や std::string の 1つの要素に、UTF-8 の文字が入る保証はありません。この辺りの事情は、「マルチバイト文字」のページで Shift_JIS を例に取り上げたことと同様です。


次のプログラムは、UTF-8 で記述した文字列リテラルの内容を test.txt に書き出しています。標準出力に出力しないのは、Windows環境での事情があるためです。

#include <fstream>

int main()
{
     std::ofstream ofs("test.txt");
     ofs << u8"あいうえお\n";
}

実行結果(test.txt の内容。UTF-8 で表示):

あいうえお

実行結果(test.txt の内容。CP932 で表示):

縺ゅ>縺�縺医♀

【C++20】前のコラムで説明していますが、C++20 以降では ofs << reinterpret_cast<const char*>(u8"あいうえお\n"); のようにしないと、コンパイルエラーになります。

test.txt を UTF-8 で開くと正しく表示されますが、CP932 で開くと滅茶苦茶な表示になります。エンコーディング形式の違いによって、正常に内容を表示したり、読んだりできなくなる現象を文字化け (Mojibake) と呼びます。

(Windows環境) コマンドプロンプトでの表示と入力

文字列を test.txt に出力して、テキストエディタでエンコーディング形式を指定して確認する方法を採りましたが、当然、結果を画面(標準出力)で確認したい場合もあります。しかし、Windows ではこれがなかなか厄介です。

u8プリフィックスを付加した文字列リテラルを、標準出力へ出力するようにして、Visual Studio 上で実行してみます。

#include <iostream>

int main()
{
    std::cout << u8"あいうえお\n";
}

実行結果(コマンドプロンプト上の表示):

縺ゅ>縺・∴縺

結果がコマンドプロンプト上で表示されますが、文字化けを起こしてしまいます。このような結果になるのは、コマンドプロンプトが CP932 を使っているためです。

これも結局は Windows の設定によるもので、「ソースファイルのエンコーディング形式」の項のコラムで取り上げた方法で UTF-8 に変更できますが、正式な機能とはなっていません。

コマンドプロンプト上で UTF-8 の文字を正常に表示させるには、設定を切り替える必要があります。コマンドプロンプトを開くたびに、設定を変更するコマンドを入力する方法もありますが、その方法では Visual Studio からの実行には対応できませんし、知識のないユーザーに毎回その対応をさせることも難しいでしょう。

コマンドプロンプト上でのコマンド入力による方法は、「Windows編>コマンドプロンプト - コードページ(言語)を切り替える」のページで取り上げています。

そこで、ソースコードの方で対応する方法があります。SetConsoleOutputCP関数7を使うと、コマンドプロンプトで表示するときに使うエンコーディング形式を指定できます。たとえば、SetConsoleOutputCP(CP_UTF8) のように呼び出すと UTF-8 になります。

SetConsoleOutputCP関数は Windows が独自に提供しているものであって、C++ の標準規格として定義されているものではありません。Windows以外ではコンパイルを通せないので注意してください。この関数を使うためには、<Windows.h> という、Windows が用意しているヘッダファイルをインクルードする必要があります。Windows でしか使えないコードは #ifdef _WIN32#endif で囲んでおけば、ほかのコンパイラでのコンパイルを妨げずに済みます。_WIN32 は、Visual Studio が独自で定義する事前定義マクロの一種です8

#include <iostream>
#ifdef _WIN32
#include <Windows.h>
#endif

int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);
#endif

    std::cout << u8"あいうえお\n";
}

実行結果(コマンドプロンプト上の表示):

あいうえお

入力の方はより深刻で、コマンドプロンプトから UTF-8 の文字列を入力して、標準入力として受け取ることは基本的にできないと思ってよさそうです9。SetConsoleOutputCP関数に対応して、入力側の設定を行う SetConsoleCP関数10が存在しますが、SetConsoleCP(CP_UTF8) としてみても、変化は起こるものの、正常な入力は得られません。

回避策として、テキストファイルに UTF-8 で文字データを書き込み、リダイレクトによって入力する方法があります(「Windows編>リダイレクト」を参照)。

新C++編では、標準入力からの入力は、ASCII で表現できる文字に限定するようにします。

min、max の問題 (NOMINMAX)

少し脱線しますが、<Windows.h> をインクルードすることによって引き起こされる有名な問題があるので、その対策を打っておきます。

その問題とは、Windows.h をインクルードすると、minマクロ11maxマクロ12が定義されることです。これらのマクロは、2つの引数を比較して小さいほう、大きいほうを返す関数形式マクロで、有用ではあるものの、名前が単純すぎて問題になっています。つまり、minmax という名前を別の用途で使おうとすると、置換されてしまって、妙なコンパイルエラーを起こすということです。たとえば、次のプログラムがコンパイルエラーになります。

#include <iostream>
#include <limits>
#ifdef _WIN32
#include <Windows.h>
#endif

int main()
{
    std::cout << std::numeric_limits<int>::min() << "\n";
    std::cout << std::numeric_limits<int>::max() << "\n";
}

std::numeric_limits の min関数、max関数を呼び出しているつもりが、minmax の部分が置換されて、コンパイルできないコードになります。Visual Studio 2015 で試すと、次のコンパイルエラーと警告が出ました。

1>  main.cpp
1>c:\main.cpp(9): warning C4003: マクロ 'min' に指定された実引数の数が少なすぎます。
1>c:\main.cpp(9): error C2589: '(': スコープ解決演算子 (::) の右側にあるトークンは使えません。
1>c:\main.cpp(9): error C2059: 構文エラー: '::'
1>c:\main.cpp(10): warning C4003: マクロ 'max' に指定された実引数の数が少なすぎます。
1>c:\main.cpp(10): error C2589: '(': スコープ解決演算子 (::) の右側にあるトークンは使えません。
1>c:\main.cpp(10): error C2059: 構文エラー: '::'

この報告内容から、Windows.h が引き起こしている問題だと気付くのはなかなか難しいでしょう。

この問題は、ヘッダファイルで定義したマクロは、利用者側に悪影響を及ぼすことがあるという良い事例になっています。原則としてマクロを使うこと自体を避けるべきですし、使うとしても、ヘッダファイルでは定義しない、名前をすべて大文字にする、ほかと被りづらい名前にするといった手を採ったほうがいいです(「プリプロセス」のページを参照)。

対策はいくつか方法がありますが、Windows.h をインクルードするより前に、NOMINMAXマクロを定義するのが簡単です。NOMINMAXマクロが定義されていると、Windows.h は minマクロと maxマクロを定義しなくなります。

#include <iostream>
#include <limits>
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
#endif

int main()
{
    std::cout << std::numeric_limits<int>::min() << "\n";
    std::cout << std::numeric_limits<int>::max() << "\n";
}

実行結果:

-2147483648
2147483647

基本的にこの対処は、つねに行って構わないと思われます。

なお、minマクロや maxマクロと同じ機能を果たす関数が、C++ の標準ライブラリにあるので、必要ならこちらを使いましょう。それぞれ、std::min関数13std::max関数14で、<algorithm> をインクルードして使います

#include <algorithm>
#include <iostream>

int main()
{
    int a {5};
    int b {3};

    std::cout << std::min(a, b) << "\n"
              << std::max(a, b) << "\n";
}

実行結果:

3
5

外部ファイルのエンコーディング形式

次に、外部のファイルのエンコーディング形式です。つまり、読み取ろうとしているファイルの中身が、どんなエンコーディング形式で書かれているのか。また、ファイルに書き込むとき、どんなエンコーディング形式で書き込むのかということです。

新C++編としては UTF-8 (BOMなし) に統一するという方針を採ります。ここでは BOM は付けません。BOM が付いていると、読み込み時に先頭の 3バイトを取り除く余計な処理が必要になるためです。

UTF-8 のテキストファイルは、これまでどおり std::ifstream や std::ofstream を使って普通に読み書きできます。

次のプログラムは、test.txt に UTF-8 で書き込んでいます。

#include <fstream>

int main()
{
    std::ofstream ofs(u8"test.txt");
    ofs << u8"あいうえお\n";
}

実行結果 (test.txt の内容。UTF-8 で表示):

あいうえお

test.txt の内容は UTF-8 で正しく読める状態になっています。

今度は test.txt を読み込んでみます。

#include <fstream>
#include <iostream>
#include <string>
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
#endif

int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);  // コマンドプロンプトでの表示に UTF-8 を使う
#endif

    std::ifstream ifs(u8"test.txt");
    std::string s {};
    ifs >> s;
    std::cout << s << u8"\n";
}

実行結果 (UTF-8 で表示):

あいうえお

読み込んだ文字列をそのまま標準出力へ出力しているだけです。結果は UTF-8 で正しく読めるようになっています。

ポーカープログラムの修正

それでは、トランプのマークを絵文字で表示できるように、ポーカープログラムを修正してみましょう。

現在のポーカープログラムは、「プリプロセス」のページの練習問題にあります。トランプのマークの文字列表現を、get_mark_string関数が返していますが、マクロの定義に応じて、アルファベット表現で返すか、カタカナ表記で返すかを切り替えられるようになっています。そこで、3つ目の選択肢として、「絵文字で返す」を追加する方針にしてみます。

切り替えに使うマクロは card.h で定義していますが、新たに CARD_MARK_STRING_EMOJI マクロを追加します。次のように修正して、3つのパターンから選べるようにしておきます。

#define CARD_MARK_STRING_EMOJI      0   // 絵文字
#define CARD_MARK_STRING_KANA       1   // カタカナ
#define CARD_MARK_STRING_ALPHABET   2   // アルファベット
#define CARD_MARK_STRING            CARD_MARK_STRING_EMOJI  // カードマークの文字列表現の方法

CARD_MARK_STRINGマクロの置換結果を、CARD_MARK_STRING_*** のいずれかに書き換えてビルドすることで、コードを切り替えます。

すべてのソースファイル、ヘッダファイルのエンコーディング形式を UTF-8 (BOM付き) に変更し、文字列リテラルには u8プリフィックスを付加しておきます。Windows環境では、コマンドプロンプトへ正常に出力できるように、SetConsoleOutputCP関数の呼び出しも必要です。

UTF-8 でなければならないはトランプのマークだけですが、画面に表示するときに選べるエンコーディング形式は1つだけですから、トランプのマーク以外の文字もエンコーディング形式を揃えておく必要があります。


最終的に次のようになります。

// main.cpp
#include <iostream>
#include <sstream>
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
#endif
#include "card.h"
#include "poker_hand.h"

constexpr CardNumber nothing_card_number = -1;  // カードがないことを表すダミー値

// 手札を配る
static void hand_out_cards(cards_type& deck, cards_type& hand_cards);

// カードを補充する
static void reload_cards(cards_type& deck, cards_type& hand_cards);

// カードの情報を出力する
static void print_cards(const cards_type& cards);

// 捨てるカードを選ぶときの出力
static void print_choose_discard(const cards_type& cards, const std::vector<bool>& is_discard);

// カードの文字列表現を返す
static std::string get_card_string(const Card& card);


int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);  // コマンドプロンプトでの表示に UTF-8 を使う
#endif

    while (true) {

        // 山札を準備
        cards_type deck(trump_card_num);
        init_trump(deck);
        shuffle_cards(deck);

        // 最初の手札を配る
        cards_type player_cards {};
        hand_out_cards(deck, player_cards);

        // ユーザーにカードを提示
        std::cout << u8"あなたに配られたカードです。\n";
        print_cards(player_cards);

        // 捨てるカードを選ばせる
        std::vector<bool> is_discard(hand_card_num);
        while (true) {
            print_choose_discard(player_cards, is_discard);

            std::string input_string {};
            std::getline(std::cin, input_string);
            if (!std::cin) {
                std::cout << u8"入力が正しくありません。\n";
            }
            else if (input_string.empty()) {
                break;
            }
            else {
                int index {input_string[0] - '0'};  // 数字から整数に変換
                if (0 <= index && index < hand_card_num) {
                    is_discard[index] = !is_discard[index];
                }
                else {
                    std::cout << u8"入力が正しくありません。\n";
                }
            }
        }

        // 選ばれたカードを捨てる
        for (std::vector<bool>::size_type i = 0; i < is_discard.size(); ++i) {
            if (is_discard.at(i)) {
                auto& card = player_cards.at(i);
                std::cout << get_card_string(card) << u8" を捨てました。\n";

                // 何もないことをあらわすダミー値を入れる
                card.number = nothing_card_number;
            }
        }
        std::cout << u8"\n";

        // 新しいカードを補充する
        reload_cards(deck, player_cards);
        sort_cards(player_cards);
        std::cout << u8"\n";
        print_cards(player_cards);

        // 役判定
        auto poker_hand = judge_poker_hand(player_cards);
        if (poker_hand == PokerHand::no_pair) {
            std::cout << u8"残念でした。\n";
        }
        else {
            std::cout << get_poker_hand_string(poker_hand) << u8"ができました。\n";
        }

        // ゲームの続行確認
        std::cout << u8"\n"
                  << u8"ゲームを続けますか?\n"
                  << u8"はい: Y  いいえ: N\n";
        std::string input_string {};
        std::getline(std::cin, input_string);
        if (input_string == u8"Y" || input_string == u8"y") {
            continue;
        }
        else {
            break;
        }
    }
}


// 手札を配る
static void hand_out_cards(cards_type& deck, cards_type& hand_cards)
{
    while (hand_cards.size() < hand_card_num) {
        hand_cards.push_back(deck.back());
        deck.pop_back();
    }
    sort_cards(hand_cards);
}

// カードを補充する
static void reload_cards(cards_type& deck, cards_type& hand_cards)
{
    // 何もないことをあらわすダミー値が入っているところに、新しいカードを補充する
    for (cards_type::size_type i = 0; i < hand_cards.size(); ++i) {
        if (hand_cards.at(i).number == nothing_card_number) {
            auto& card = deck.back();

            std::cout << get_card_string(card) << u8" が配られました。\n";
            hand_cards.at(i) = card;
            deck.pop_back();
        }
    }
}

// カードの情報を出力する
static void print_cards(const cards_type& cards)
{
    for (cards_type::size_type i = 0; i < cards.size(); ++i) {
        auto& card {cards.at(i)};

        if (card.number == nothing_card_number) {
            std::cout << i << u8": \n";
        }
        else {
            std::cout << i << u8": " << get_card_string(card) << u8"\n";
        }
    }
}

// 捨てるカードを選ぶときの出力
static void print_choose_discard(const cards_type& cards, const std::vector<bool>& is_discard)
{
    std::cout << u8"\n"
              << u8"左端の番号を入力して、カードを選んでください。\n"
              << u8"何も入力せず Enterキーを押すと決定します。\n";

    for (cards_type::size_type i = 0; i < cards.size(); ++i) {
        std::cout << i << u8": " << get_card_string(cards.at(i));

        // 捨てるカードに含まれているか
        if (is_discard[i]) {
            std::cout << u8" <-- 捨てる";
        }

        std::cout << u8"\n";
    }
}

// カードの文字列表現を返す
static std::string get_card_string(const Card& card)
{
#if CARD_MARK_STRING == CARD_MARK_STRING_KANA
    return get_mark_string(card.mark) + u8"の" + get_card_number_string(card.number);
#else
    return get_mark_string(card.mark) + u8" " + get_card_number_string(card.number);
#endif
}
// poker_hand.cpp
#include "poker_hand.h"
#include <algorithm>
#include <cassert>

// ストレートが成立しているか判定する
static bool is_straight(const cards_type& cards)
{
    // [!] 1 を巻き込むストレートは、1・10・11・12・13 か 1・2・3・4・5 のいずれかしか認めないルールもあるが、
    //     ここでは、とにかく連続していればいいことにしている。

    // 13 から 1 に戻ってくるケースを先に判定
    if (cards[0].number == 1 && cards[4].number == 13) {
        return cards[1].number ==  2 && cards[2].number ==  3 && cards[3].number ==  4
            || cards[1].number ==  2 && cards[2].number ==  3 && cards[3].number == 12
            || cards[1].number ==  2 && cards[2].number == 11 && cards[3].number == 12
            || cards[1].number == 10 && cards[2].number == 11 && cards[3].number == 12
            ;
    }

    return cards[0].number + 1 == cards[1].number
        && cards[1].number + 1 == cards[2].number
        && cards[2].number + 1 == cards[3].number
        && cards[3].number + 1 == cards[4].number
        ;
}

// 役を判定する
PokerHand judge_poker_hand(const cards_type& cards)
{
    assert(cards.size() == hand_card_num);
    assert(std::is_sorted(std::cbegin(cards), std::cend(cards), cards_sort_compare));

    // フラッシュ系
    if (cards[0].mark == cards[1].mark
     && cards[0].mark == cards[2].mark
     && cards[0].mark == cards[3].mark
     && cards[0].mark == cards[4].mark
    ) {
        // ロイヤルストレートフラッシュ
        if (cards[0].number == 1
         && cards[1].number == 10
         && cards[2].number == 11
         && cards[3].number == 12
         && cards[4].number == 13
        ) {
            return PokerHand::royal_straight_flush;
        }

        // ストレートフラッシュ
        if (is_straight(cards)) {
            return PokerHand::straight_flush;
        }
        
        // フラッシュ
        return PokerHand::flush;
    }

    // ストレート
    if (is_straight(cards)) {
        return PokerHand::straight;
    }

    // 数字ごとのカードの枚数を数える
    std::vector<int> number_count(each_mark_card_num + 1);
    for (auto& card : cards) {
        number_count.at(card.number) += 1;
    }

    // 一番枚数が多い数字
    int number_count_max {*std::max_element(std::cbegin(number_count), std::cend(number_count))};

    // フォーカード
    if (number_count_max == 4) {
        return PokerHand::four_cards;
    }

    // フルハウス or スリーカード
    if (number_count_max == 3) {

        // 2枚ある番号が存在すればフルハウス
        if (std::find(std::cbegin(number_count), std::cend(number_count), 2) != std::cend(number_count)) {
            return PokerHand::full_house;
        }

        // スリーカード
        return PokerHand::three_cards;
    }

    // ツーペア or ワンペア
    if (number_count_max == 2) {

        // 2枚ある番号が2つあればツーペア
        if (std::count(std::cbegin(number_count), std::cend(number_count), 2) == 2) {
            return PokerHand::two_pairs;
        }

        // ワンペア
        return PokerHand::one_pair;
    }

    // ノーペア
    return PokerHand::no_pair;
}

// 役の文字列表現を返す
std::string get_poker_hand_string(PokerHand poker_hand)
{
    switch (poker_hand) {
    case PokerHand::no_pair:                return u8"ノーペア";
    case PokerHand::one_pair:               return u8"ワンペア";
    case PokerHand::two_pairs:              return u8"ツーペア";
    case PokerHand::three_cards:            return u8"スリーカード";
    case PokerHand::straight:               return u8"ストレート";
    case PokerHand::flush:                  return u8"フラッシュ";
    case PokerHand::full_house:             return u8"フルハウス";
    case PokerHand::four_cards:             return u8"フォーカード";
    case PokerHand::straight_flush:         return u8"ストレートフラッシュ";
    case PokerHand::royal_straight_flush:   return u8"ロイヤルストレートフラッシュ";
    default:                                return u8"";
    }
}
// poker_hand.h
#ifndef POKER_HAND_H_INCLUDED
#define POKER_HAND_H_INCLUDED

#include <string>
#include "card.h"

constexpr auto hand_card_num = 5;               // 手札の枚数

// 役
enum class PokerHand : signed char {
    no_pair,
    one_pair,
    two_pairs,
    three_cards,
    straight,
    flush,
    full_house,
    four_cards,
    straight_flush,
    royal_straight_flush,
};

// 役を判定する
// cards: 手札 (要素数は 5。card.h の cards_sort_compare() が定義する順序で整列されていなければならない)
// 戻り値: 役
PokerHand judge_poker_hand(const cards_type& cards);

// 役の文字列表現を返す
// poker_hand: 役
// 戻り値: 役の名前
std::string get_poker_hand_string(PokerHand poker_hand);

#endif
// card.cpp
#include "card.h"
#include <algorithm>
#include <random>

// トランプを初期化する
void init_trump(cards_type& cards)
{
    for (auto i = 0; i < each_mark_card_num; ++i) {
        auto number = static_cast<CardNumber>(i + 1);
        cards.at(i + each_mark_card_num * 0).number = number;
        cards.at(i + each_mark_card_num * 0).mark = CardMark::spade;
        cards.at(i + each_mark_card_num * 1).number = number;
        cards.at(i + each_mark_card_num * 1).mark = CardMark::club;
        cards.at(i + each_mark_card_num * 2).number = number;
        cards.at(i + each_mark_card_num * 2).mark = CardMark::diamond;
        cards.at(i + each_mark_card_num * 3).number = number;
        cards.at(i + each_mark_card_num * 3).mark = CardMark::heart;
    }
}

// カードをシャッフルする
void shuffle_cards(cards_type& cards)
{
    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());
    std::shuffle(std::begin(cards), std::end(cards), rand_engine);
}

// カードを整列する
void sort_cards(cards_type& cards)
{
    std::sort(std::begin(cards), std::end(cards), cards_sort_compare);
}

// カードの整列に使う比較関数
bool cards_sort_compare(const Card& a, const Card& b)
{
    if (a.number == b.number) {
        return a.mark < b.mark;
    }
    return a.number < b.number;
}

// カードの数字の文字列表現を返す
std::string get_card_number_string(CardNumber number)
{
    switch (number) {
    case 1:     return u8"A";
    case 2:     return u8"2";
    case 3:     return u8"3";
    case 4:     return u8"4";
    case 5:     return u8"5";
    case 6:     return u8"6";
    case 7:     return u8"7";
    case 8:     return u8"8";
    case 9:     return u8"9";
    case 10:    return u8"10";
    case 11:    return u8"J";
    case 12:    return u8"Q";
    case 13:    return u8"K";
    default:    return u8"";
    }
}

// カードのマークの文字列表現を返す
std::string get_mark_string(CardMark card_mark)
{
#if CARD_MARK_STRING == CARD_MARK_STRING_EMOJI
    switch (card_mark) {
    case CardMark::spade:
        return u8"♠";
    case CardMark::club:
        return u8"♣";
    case CardMark::diamond:
        return u8"♦";
    case CardMark::heart:
        return u8"♥";
    default:
        return u8"";
    }
#elif CARD_MARK_STRING == CARD_MARK_STRING_KANA
    switch (card_mark) {
    case CardMark::spade:
        return u8"スペード";
    case CardMark::club:
        return u8"クラブ";
    case CardMark::diamond:
        return u8"ダイヤ";
    case CardMark::heart:
        return u8"ハート";
    default:
        return u8"";
    }
#elif CARD_MARK_STRING == CARD_MARK_STRING_ALPHABET
    switch (card_mark) {
    case CardMark::spade:
        return u8"spade";
    case CardMark::club:
        return u8"club";
    case CardMark::diamond:
        return u8"diamond";
    case CardMark::heart:
        return u8"heart";
    default:
        return u8"";
    }
#else
    #error "invalid CARD_MARK_STRING."
#endif
}
// card.h
#ifndef CARD_H_INCLUDED
#define CARD_H_INCLUDED

#include <string>
#include <vector>

#define CARD_MARK_STRING_EMOJI      0   // 絵文字
#define CARD_MARK_STRING_KANA       1   // カタカナ
#define CARD_MARK_STRING_ALPHABET   2   // アルファベット
#define CARD_MARK_STRING            CARD_MARK_STRING_EMOJI  // カードマークの文字列表現の方法

// カードの数字の型
using CardNumber = signed char;

// カードのマークの型
enum class CardMark : signed char {
    spade,
    club,
    diamond,
    heart,
};

// カード型
struct Card {
    CardNumber number;  // 数字
    CardMark mark;      // マーク
};

// カードの集まりを表す型
using cards_type = std::vector<Card>;

constexpr auto card_mark_num = 4;                                    // マークの総数
constexpr auto each_mark_card_num = 13;                              // 各マークのカードの枚数
constexpr auto trump_card_num = each_mark_card_num * card_mark_num;  // トランプに含まれるカードの枚数


// トランプを初期化する
// cards: カードの集まり
void init_trump(cards_type& cards);

// カードをシャッフルする
// cards: カードの集まり
void shuffle_cards(cards_type& cards);

// カードを整列する
// cards: カードの集まり
void sort_cards(cards_type& cards);

// カードの整列に使う比較関数
// a: 対象のカード
// b: 対象のカード
// 戻り値: a と b が適切な順番で並んでいたら true、並んでいなければ false
bool cards_sort_compare(const Card& a, const Card& b);

// カードの数字の文字列表現を返す
// たとえば、3 は "3"、1 は "A"、13 は "K" と表現される。
//
// number: カードの数字
// 戻り値: カードの数字を文字列で表現したもの
std::string get_card_number_string(CardNumber number);

// カードのマークの文字列表現を返す
// 具体的な表現方法は、CARD_MARK_STRING マクロの定義によって異なる。
// 
// card_mark: カードのマーク
// 戻り値: カードのマークを文字列で表現したもの
std::string get_mark_string(CardMark card_mark);

#endif

実行結果は次のようになります。

あなたに配られたカードです。
0: ♦ A
1: ♦ 3
2: ♦ 8
3: ♦ 9
4: ♠ K

(以下略)

【上級】【C++20】UTF-8 に関する仕様変更の問題

C++20 では、u8プリフィックス付きの文字リテラル、文字列リテラルの型が char8_t に変更されました。その一方で、std::cout に char8_t型の文字や文字列を渡すことはできません15。そのため、さきほどのポーカープログラムは C++20 ではコンパイルが通らなくなっています。

std::cout に char8_t の文字や文字列を渡すには、std::cout << reinterpret_cast<const char*>(u8"あいうえお"); のようにキャストする方法があります。これは、UTF-8 でエンコーディングされている文字列を、バイト列とみなして std::cout に渡すことを意味しています(「配列とポインタ」のページを参照)。出力先がこのバイト列を UTF-8 であるとみなして取り扱えば(たとえば、コマンドプロンプトが UTF-8 を表示するように設定されていれば)、正しい結果が得られます。

ただ、出力のたびにこのような記述を書かされるのでは面倒ですし、reinterpret_cast をばらまくのも嫌なので、以下のような関数テンプレートを用意して対処することが考えられます。

inline std::ostream& operator<<(std::ostream& os, const char8_t* s) {
    return os << reinterpret_cast<const char*>(s);
}
inline std::ostream& operator<<(std::ostream& os, const std::u8string& s) {
    return os << s.c_str();
}

// 1文字版
inline std::ostream& operator<<(std::ostream& os, utf8_char s) {
    return os << static_cast<char>(s);
}

これがみえるところにあれば、std::cout << u8"あいうえお"; は上の関数を呼び出すようになります。

これがどのような仕組みで動くのかはまだ解説していませんが、ようは std::cout の << の動作を、特定の型のときだけ変更するということをしています。本当はほかにも対応が必要になる関数がありますが、ここでは std::cout の << だけを対応しています。

ポーカープログラムのソースコードを、C++14~20 の各バージョンに対応することを狙うならば、次のようなヘッダファイルを用意するといいでしょう。

// strean.h
#ifndef STREAM_H_INCLUDED
#define STREAM_H_INCLUDED

#include <iostream>
#include <string>

#ifdef __cpp_char8_t

// UTF-8 を扱うための型
using utf8_char = char8_t;
using utf8_string = std::u8string;

// UTF-8 の文字、文字列を出力する
inline std::ostream& operator<<(std::ostream& os, utf8_char s) {
    return os << static_cast<char>(s);
}
inline std::ostream& operator<<(std::ostream& os, const utf8_char* s) {
    return os << reinterpret_cast<const char*>(s);
}
inline std::ostream& operator<<(std::ostream& os, const utf8_string& s) {
    return os << s.c_str();
}
#else

// UTF-8 を扱うための型
using utf8_char = char;
using utf8_string = std::string;

#endif

#endif

__cpp_char8_t は機能テストマクロ(「プリプロセス」のページを参照)で、char8_t が使えるようになっている処理系でのみ定義されます。このマクロが定義されている環境でのみ、さきほどの関数テンプレートを定義します。

また、char と char8_t を使い分けなければならないので、utf8_char や utf8_string といった別名を定義して、UTF-8 でなければならない箇所では、この型名を使うように統一します。__cpp_char8_tマクロが定義されない処理系では、char8_t が使えないということですから、utf8_char は char、utf8_string は std::string を意味するように定義しておきます。

ほかのソースファイルは次のようになります。

// main.cpp
#include <iostream>
#include <sstream>
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
#endif
#include "card.h"
#include "poker_hand.h"
#include "stream.h"

constexpr CardNumber nothing_card_number = -1;  // カードがないことを表すダミー値

// 手札を配る
static void hand_out_cards(cards_type& deck, cards_type& hand_cards);

// カードを補充する
static void reload_cards(cards_type& deck, cards_type& hand_cards);

// カードの情報を出力する
static void print_cards(const cards_type& cards);

// 捨てるカードを選ぶときの出力
static void print_choose_discard(const cards_type& cards, const std::vector<bool>& is_discard);

// カードの文字列表現を返す
static utf8_string get_card_string(const Card& card);


int main()
{
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);  // コマンドプロンプトでの表示に UTF-8 を使う
#endif

    while (true) {

        // 山札を準備
        cards_type deck(trump_card_num);
        init_trump(deck);
        shuffle_cards(deck);

        // 最初の手札を配る
        cards_type player_cards {};
        hand_out_cards(deck, player_cards);

        // ユーザーにカードを提示
        std::cout << u8"あなたに配られたカードです。\n";
        print_cards(player_cards);

        // 捨てるカードを選ばせる
        std::vector<bool> is_discard(hand_card_num);
        while (true) {
            print_choose_discard(player_cards, is_discard);

            std::string input_string {};
            std::getline(std::cin, input_string);
            if (!std::cin) {
                std::cout << u8"入力が正しくありません。\n";
            }
            else if (input_string.empty()) {
                break;
            }
            else {
                int index {input_string[0] - '0'};  // 数字から整数に変換
                if (0 <= index && index < hand_card_num) {
                    is_discard[index] = !is_discard[index];
                }
                else {
                    std::cout << u8"入力が正しくありません。\n";
                }
            }
        }

        // 選ばれたカードを捨てる
        for (std::vector<bool>::size_type i = 0; i < is_discard.size(); ++i) {
            if (is_discard.at(i)) {
                auto& card = player_cards.at(i);
                std::cout << get_card_string(card) << u8" を捨てました。\n";

                // 何もないことをあらわすダミー値を入れる
                card.number = nothing_card_number;
            }
        }
        std::cout << u8"\n";

        // 新しいカードを補充する
        reload_cards(deck, player_cards);
        sort_cards(player_cards);
        std::cout << u8"\n";
        print_cards(player_cards);

        // 役判定
        auto poker_hand = judge_poker_hand(player_cards);
        if (poker_hand == PokerHand::no_pair) {
            std::cout << u8"残念でした。\n";
        }
        else {
            std::cout << get_poker_hand_string(poker_hand) << u8"ができました。\n";
        }

        // ゲームの続行確認
        std::cout << u8"\n"
                  << u8"ゲームを続けますか?\n"
                  << u8"はい: Y  いいえ: N\n";
        std::string input_string {};
        std::getline(std::cin, input_string);
        if (input_string == "Y" || input_string == "y") {
            continue;
        }
        else {
            break;
        }
    }
}


// 手札を配る
static void hand_out_cards(cards_type& deck, cards_type& hand_cards)
{
    while (hand_cards.size() < hand_card_num) {
        hand_cards.push_back(deck.back());
        deck.pop_back();
    }
    sort_cards(hand_cards);
}

// カードを補充する
static void reload_cards(cards_type& deck, cards_type& hand_cards)
{
    // 何もないことをあらわすダミー値が入っているところに、新しいカードを補充する
    for (cards_type::size_type i = 0; i < hand_cards.size(); ++i) {
        if (hand_cards.at(i).number == nothing_card_number) {
            auto& card = deck.back();

            std::cout << get_card_string(card) << u8" が配られました。\n";
            hand_cards.at(i) = card;
            deck.pop_back();
        }
    }
}

// カードの情報を出力する
static void print_cards(const cards_type& cards)
{
    for (cards_type::size_type i = 0; i < cards.size(); ++i) {
        auto& card {cards.at(i)};

        if (card.number == nothing_card_number) {
            std::cout << i << u8": \n";
        }
        else {
            std::cout << i << u8": " << get_card_string(card) << u8"\n";
        }
    }
}

// 捨てるカードを選ぶときの出力
static void print_choose_discard(const cards_type& cards, const std::vector<bool>& is_discard)
{
    std::cout << u8"\n"
              << u8"左端の番号を入力して、カードを選んでください。\n"
              << u8"何も入力せず Enterキーを押すと決定します。\n";

    for (cards_type::size_type i = 0; i < cards.size(); ++i) {
        std::cout << i << u8": " << get_card_string(cards.at(i));

        // 捨てるカードに含まれているか
        if (is_discard[i]) {
            std::cout << u8" <-- 捨てる";
        }

        std::cout << u8"\n";
    }
}

// カードの文字列表現を返す
static utf8_string get_card_string(const Card& card)
{
#if CARD_MARK_STRING == CARD_MARK_STRING_KANA
    return get_mark_string(card.mark) + u8"の" + get_card_number_string(card.number);
#else
    return get_mark_string(card.mark) + u8" " + get_card_number_string(card.number);
#endif
}
// poker_hand.cpp
#include "poker_hand.h"
#include <algorithm>
#include <cassert>

// ストレートが成立しているか判定する
static bool is_straight(const cards_type& cards)
{
    // [!] 1 を巻き込むストレートは、1・10・11・12・13 か 1・2・3・4・5 のいずれかしか認めないルールもあるが、
    //     ここでは、とにかく連続していればいいことにしている。

    // 13 から 1 に戻ってくるケースを先に判定
    if (cards[0].number == 1 && cards[4].number == 13) {
        return cards[1].number ==  2 && cards[2].number ==  3 && cards[3].number ==  4
            || cards[1].number ==  2 && cards[2].number ==  3 && cards[3].number == 12
            || cards[1].number ==  2 && cards[2].number == 11 && cards[3].number == 12
            || cards[1].number == 10 && cards[2].number == 11 && cards[3].number == 12
            ;
    }

    return cards[0].number + 1 == cards[1].number
        && cards[1].number + 1 == cards[2].number
        && cards[2].number + 1 == cards[3].number
        && cards[3].number + 1 == cards[4].number
        ;
}

// 役を判定する
PokerHand judge_poker_hand(const cards_type& cards)
{
    assert(cards.size() == hand_card_num);
    assert(std::is_sorted(std::cbegin(cards), std::cend(cards), cards_sort_compare));

    // フラッシュ系
    if (cards[0].mark == cards[1].mark
     && cards[0].mark == cards[2].mark
     && cards[0].mark == cards[3].mark
     && cards[0].mark == cards[4].mark
    ) {
        // ロイヤルストレートフラッシュ
        if (cards[0].number == 1
         && cards[1].number == 10
         && cards[2].number == 11
         && cards[3].number == 12
         && cards[4].number == 13
        ) {
            return PokerHand::royal_straight_flush;
        }

        // ストレートフラッシュ
        if (is_straight(cards)) {
            return PokerHand::straight_flush;
        }
        
        // フラッシュ
        return PokerHand::flush;
    }

    // ストレート
    if (is_straight(cards)) {
        return PokerHand::straight;
    }

    // 数字ごとのカードの枚数を数える
    std::vector<int> number_count(each_mark_card_num + 1);
    for (auto& card : cards) {
        number_count.at(card.number) += 1;
    }

    // 一番枚数が多い数字
    int number_count_max {*std::max_element(std::cbegin(number_count), std::cend(number_count))};

    // フォーカード
    if (number_count_max == 4) {
        return PokerHand::four_cards;
    }

    // フルハウス or スリーカード
    if (number_count_max == 3) {

        // 2枚ある番号が存在すればフルハウス
        if (std::find(std::cbegin(number_count), std::cend(number_count), 2) != std::cend(number_count)) {
            return PokerHand::full_house;
        }

        // スリーカード
        return PokerHand::three_cards;
    }

    // ツーペア or ワンペア
    if (number_count_max == 2) {

        // 2枚ある番号が2つあればツーペア
        if (std::count(std::cbegin(number_count), std::cend(number_count), 2) == 2) {
            return PokerHand::two_pairs;
        }

        // ワンペア
        return PokerHand::one_pair;
    }

    // ノーペア
    return PokerHand::no_pair;
}

// 役の文字列表現を返す
utf8_string get_poker_hand_string(PokerHand poker_hand)
{
    switch (poker_hand) {
    case PokerHand::no_pair:                return u8"ノーペア";
    case PokerHand::one_pair:               return u8"ワンペア";
    case PokerHand::two_pairs:              return u8"ツーペア";
    case PokerHand::three_cards:            return u8"スリーカード";
    case PokerHand::straight:               return u8"ストレート";
    case PokerHand::flush:                  return u8"フラッシュ";
    case PokerHand::full_house:             return u8"フルハウス";
    case PokerHand::four_cards:             return u8"フォーカード";
    case PokerHand::straight_flush:         return u8"ストレートフラッシュ";
    case PokerHand::royal_straight_flush:   return u8"ロイヤルストレートフラッシュ";
    default:                                return u8"";
    }
}
// poker_hand.h
#ifndef POKER_HAND_H_INCLUDED
#define POKER_HAND_H_INCLUDED

#include <string>
#include "card.h"
#include "stream.h"

constexpr auto hand_card_num = 5;               // 手札の枚数

// 役
enum class PokerHand : signed char {
    no_pair,
    one_pair,
    two_pairs,
    three_cards,
    straight,
    flush,
    full_house,
    four_cards,
    straight_flush,
    royal_straight_flush,
};

// 役を判定する
// cards: 手札 (要素数は 5。card.h の cards_sort_compare() が定義する順序で整列されていなければならない)
// 戻り値: 役
PokerHand judge_poker_hand(const cards_type& cards);

// 役の文字列表現を返す
// poker_hand: 役
// 戻り値: 役の名前
utf8_string get_poker_hand_string(PokerHand poker_hand);

#endif
// card.cpp
#include "card.h"
#include <algorithm>
#include <random>

// トランプを初期化する
void init_trump(cards_type& cards)
{
    for (auto i = 0; i < each_mark_card_num; ++i) {
        auto number = static_cast<CardNumber>(i + 1);
        cards.at(i + each_mark_card_num * 0).number = number;
        cards.at(i + each_mark_card_num * 0).mark = CardMark::spade;
        cards.at(i + each_mark_card_num * 1).number = number;
        cards.at(i + each_mark_card_num * 1).mark = CardMark::club;
        cards.at(i + each_mark_card_num * 2).number = number;
        cards.at(i + each_mark_card_num * 2).mark = CardMark::diamond;
        cards.at(i + each_mark_card_num * 3).number = number;
        cards.at(i + each_mark_card_num * 3).mark = CardMark::heart;
    }
}

// カードをシャッフルする
void shuffle_cards(cards_type& cards)
{
    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());
    std::shuffle(std::begin(cards), std::end(cards), rand_engine);
}

// カードを整列する
void sort_cards(cards_type& cards)
{
    std::sort(std::begin(cards), std::end(cards), cards_sort_compare);
}

// カードの整列に使う比較関数
bool cards_sort_compare(const Card& a, const Card& b)
{
    if (a.number == b.number) {
        return a.mark < b.mark;
    }
    return a.number < b.number;
}

// カードの数字の文字列表現を返す
utf8_string get_card_number_string(CardNumber number)
{
    switch (number) {
    case 1:     return u8"A";
    case 2:     return u8"2";
    case 3:     return u8"3";
    case 4:     return u8"4";
    case 5:     return u8"5";
    case 6:     return u8"6";
    case 7:     return u8"7";
    case 8:     return u8"8";
    case 9:     return u8"9";
    case 10:    return u8"10";
    case 11:    return u8"J";
    case 12:    return u8"Q";
    case 13:    return u8"K";
    default:    return u8"";
    }
}

// カードのマークの文字列表現を返す
utf8_string get_mark_string(CardMark card_mark)
{
#if CARD_MARK_STRING == CARD_MARK_STRING_EMOJI
    switch (card_mark) {
    case CardMark::spade:
        return u8"♠";
    case CardMark::club:
        return u8"♣";
    case CardMark::diamond:
        return u8"♦";
    case CardMark::heart:
        return u8"♥";
    default:
        return u8"";
    }
#elif CARD_MARK_STRING == CARD_MARK_STRING_KANA
    switch (card_mark) {
    case CardMark::spade:
        return u8"スペード";
    case CardMark::club:
        return u8"クラブ";
    case CardMark::diamond:
        return u8"ダイヤ";
    case CardMark::heart:
        return u8"ハート";
    default:
        return u8"";
    }
#elif CARD_MARK_STRING == CARD_MARK_STRING_ALPHABET
    switch (card_mark) {
    case CardMark::spade:
        return u8"spade";
    case CardMark::club:
        return u8"club";
    case CardMark::diamond:
        return u8"diamond";
    case CardMark::heart:
        return u8"heart";
    default:
        return u8"";
    }
#else
    #error "invalid CARD_MARK_STRING."
#endif
}
// card.h
#ifndef CARD_H_INCLUDED
#define CARD_H_INCLUDED

#include <string>
#include <vector>
#include "stream.h"

#define CARD_MARK_STRING_EMOJI      0   // 絵文字
#define CARD_MARK_STRING_KANA       1   // カタカナ
#define CARD_MARK_STRING_ALPHABET   2   // アルファベット
#define CARD_MARK_STRING            CARD_MARK_STRING_EMOJI  // カードマークの文字列表現の方法

// カードの数字の型
using CardNumber = signed char;

// カードのマークの型
enum class CardMark : signed char {
    spade,
    club,
    diamond,
    heart,
};

// カード型
struct Card {
    CardNumber number;  // 数字
    CardMark mark;      // マーク
};

// カードの集まりを表す型
using cards_type = std::vector<Card>;

constexpr auto card_mark_num = 4;                                    // マークの総数
constexpr auto each_mark_card_num = 13;                              // 各マークのカードの枚数
constexpr auto trump_card_num = each_mark_card_num * card_mark_num;  // トランプに含まれるカードの枚数


// トランプを初期化する
// cards: カードの集まり
void init_trump(cards_type& cards);

// カードをシャッフルする
// cards: カードの集まり
void shuffle_cards(cards_type& cards);

// カードを整列する
// cards: カードの集まり
void sort_cards(cards_type& cards);

// カードの整列に使う比較関数
// a: 対象のカード
// b: 対象のカード
// 戻り値: a と b が適切な順番で並んでいたら true、並んでいなければ false
bool cards_sort_compare(const Card& a, const Card& b);

// カードの数字の文字列表現を返す
// たとえば、3 は "3"、1 は "A"、13 は "K" と表現される。
//
// number: カードの数字
// 戻り値: カードの数字を文字列で表現したもの
utf8_string get_card_number_string(CardNumber number);

// カードのマークの文字列表現を返す
// 具体的な表現方法は、CARD_MARK_STRING マクロの定義によって異なる。
// 
// card_mark: カードのマーク
// 戻り値: カードのマークを文字列で表現したもの
utf8_string get_mark_string(CardMark card_mark);

#endif

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

ASCII で正常に読めるように作られたテキストファイルを、UTF-8 (BOM付き) で保存しなおしたら、ファイルの大きさは増えますか?

解答・解説

問題2 (応用★★)

UTF-8 (BOM付き) のテキストファイルを用意して、そのファイルの内容を読み取ると、どうなるか確認してみてください。また、BOM を取り除いた部分だけを読み取るにはどうすれば良いでしょうか?

解答・解説

問題3 (調査★★)

UTF-8 で、トランプのマークを1つ表現するには、何バイト必要でしょうか?

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る