分割コンパイル | Programming Place Plus 新C++編

トップページ新C++編

このページの概要 🔗

このページでは、ソースコードを複数のソースファイルに分割して記述する方法を取り上げます。まずは単純に、1つのソースファイルにすべてのコードを記述すると大きくなりすぎるので、単純に2つのファイルに分けるとしたらどうなるかを確認します。より本格的な方法は次回にまわします。

このページの解説は C++14 をベースとしています

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



複数のソースファイル 🔗

前のページまでで、トランプのカードを表現できるようになり、山札までは用意できています。手札を表現することも、要素数が 5 の std::vector<Card> とすればいいだけなので簡単です。手札は、前のページの方法で、整列してプレイヤーに提示するといいでしょう。

残された大きめの課題は役の判定です。ポーカーのルールは色々ありますが、ここでは以下の役があることを想定します。下にいくほど、強い役です。

これらの役を scoped enum で表現するとして、手札の情報から、当てはまる役を判定する関数を作ります。

// 役を判定する
// cards: 手札(要素数は 5 で、整列済みでなければならない)
// 戻り値: 役
PokerHand judge_poker_hand(const std::vector<Card>& cards);

judge_poker_hand関数はそれなりに規模が大きくなります。また、1度作ってしまえば、ポーカーのルールを変えないかぎりはずっと使いまわせるソースコードになるはずです。そこで、役判定に関するソースコードを、別のソースファイルに分離することにします。つまり、main関数があるソースファイルのほかに、もう1つソースファイルがある構造になります。

どこかのソースファイルに main関数がなければいけません。また、main関数があちこちにあってもいけません。

このようなプログラムを Visual Studio で作成するには、プロジェクトに複数のソースファイルを登録すればいいだけです。ビルドや実行の方法はこれまでどおりです。プロジェクトにソースファイルを追加する方法は、「Visual Studio編>ソースファイルを追加する」を参照してください。

役判定のコードを集めたソースファイル (poker_hand.cpp とします) の内容は次のようになります。

#include <algorithm>
#include <string>
#include <vector>

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

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

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

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

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;  // トランプに含まれるカードの枚数
constexpr auto hand_card_num = 5;                                    // 手札の枚数

// ストレートが成立しているか判定する
bool is_straight(const std::vector<Card>& cards)
{
    // 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 std::vector<Card>& cards)
{
    if (cards.size() != hand_card_num) {
        return PokerHand::no_pair;
    }

    // フラッシュ系
    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 "ノーペア";
    case PokerHand::one_pair:               return "ワンペア";
    case PokerHand::two_pairs:              return "ツーペア";
    case PokerHand::three_cards:            return "スリーカード";
    case PokerHand::straight:               return "ストレート";
    case PokerHand::flush:                  return "フラッシュ";
    case PokerHand::full_house:             return "フルハウス";
    case PokerHand::four_cards:             return "フォーカード";
    case PokerHand::straight_flush:         return "ストレートフラッシュ";
    case PokerHand::royal_straight_flush:   return "ロイヤルストレートフラッシュ";
    default:                                return "";
    }
}

main関数を含んだソースファイル(main.cpp とします)が必要です。シャッフルされた山札から 5枚のカードを配り、整列し、役判定を行うようにしてみます。

#include <algorithm>
#include <iostream>
#include <random>
#include <string>
#include <vector>

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

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

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

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

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

// poker_hand.cpp にある関数の宣言
PokerHand judge_poker_hand(const std::vector<Card>& cards);
std::string get_poker_hand_string(PokerHand poker_hand);

// トランプを初期化する
void init_trump(std::vector<Card>& 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 hand_out_cards(std::vector<Card>& deck, std::vector<Card>& hand_cards)
{
    while (hand_cards.size() < hand_card_num) {
        hand_cards.push_back(deck.back());
        deck.pop_back();
    }

    // 整列
    std::sort(std::begin(hand_cards), std::end(hand_cards),
        [](const Card& a, const Card& b){
            if (a.number == b.number) {
                return a.mark < b.mark;
            }
            return a.number < b.number;
        }
    );
}

// カードのマークの文字列表現を返す
std::string get_mark_string(CardMark card_mark)
{
    switch (card_mark) {
    case CardMark::spade:
        return "spade";
    case CardMark::club:
        return "club";
    case CardMark::diamond:
        return "diamond";
    case CardMark::heart:
        return "heart";
    default:
        return "";
    }
}

int main()
{
    std::vector<Card> deck(trump_card_num);
    init_trump(deck);

    // 山札のカードをシャッフルする
    std::random_device rand_dev {};
    std::mt19937 rand_engine(rand_dev());
    std::shuffle(std::begin(deck), std::end(deck), rand_engine);
    
    // 手札を配る
    std::vector<Card> player_cards {};
    hand_out_cards(deck, player_cards);

    // 手札の内容を表示
    for (auto& card : player_cards) {
        std::cout << get_mark_string(card.mark) << " : " << static_cast<int>(card.number) << "\n";
    }

    // 役判定
    auto judge = judge_poker_hand(player_cards);
    std::cout << get_poker_hand_string(judge) << "\n";
}

実行結果:

diamond : 4
spade : 10
heart : 10
club : 12
diamond : 12
ツーペア

実行するたびに手札の内容が変わり、役判定の結果が出力されます。


main.cpp から、poker_hand.cpp にある関数を呼びだすために、関数の宣言が見えていなければなりません。そのため、main.cpp の側に宣言を書いています。ほかのソースファイルにある関数であることを分かりやすくするために、宣言の先頭に externキーワードを付加する方法もあります。

extern PokerHand judge_poker_hand(const std::vector<Card>& cards);
extern std::string get_poker_hand_string(PokerHand poker_hand);

extern を付加しても意味は変わりませんが、これが宣言であることが明確になり、定義はほかのところにあるといっていることにもなります。

こうして、ほかのソースファイルにある関数を呼び出せますが、この方法はあまり良いものではありません。より適切な方法を、次のページで取り上げることにします。また、同じ型の定義を2つのソースファイルに重複して記述していることも気になりますが、これについても次のページで解決を図ります。

ビルドの過程 🔗

Visual Studio では、ビルドのコマンドだけで、実行ファイルの生成まで完了します。実際にはいくつか踏まなければならない過程があって、ビルドはそれを順番に行うコマンドです。この過程の中から、コンパイルとリンクを説明しておきます。

コンパイル 🔗

C++ で書かれたソースファイルをコンパイルすることによって、オブジェクトファイル (object file) が生成されます。

Visual Studio であれば、ソースファイルの名前に対応した .obj という拡張子のファイルが生成されています(main.cpp に対して main.obj といったように)。

ソースファイルが複数あるとしても、全部一括でコンパイルしているのではなく、ソースファイルを1つずつコンパイルします。そのため、あるソースファイルをコンパイルしているとき、ほかのソースファイルの存在はみえていません。このような手法は、分割コンパイル (separate compilation) と呼びます。

main.cpp をコンパイルしているとき、sub.cpp のことは見えていないので、main.cpp から sub.cpp にある関数を呼び出すコードを書いたものの、実は sub.cpp にその関数の定義が記述されていなかったとしても、コンパイルエラーとしては検知できません。

// main.cpp

extern int f1(int x);  // sub.cpp にあるはずの関数の宣言

int main()
{
    f1(10);  // f1() の宣言がみえているのでコンパイルできる
}
// sub.cpp

// f1 ではなく f2 の定義。f1 の定義はどこにもない
int f2(int x)
{
    return x;
}

main.cpp だけをみるとコンパイル可能なコードですし、sub.cpp だけをみてもやはりコンパイル可能なコードです。実際、どちらのソースファイルもコンパイルは成功しますが、この次のリンクの過程で f1 の定義がないことが発覚してエラーになります。

Visual Studio でコンパイルだけを行う方法は、VisualStudio編「コンパイルを行う」のページを参照してください。


なお、ビルド時間を削減するために、最後にコンパイルされたあと、変更のないソースファイルについてはコンパイルされないのが普通です。そのため、プログラムを複数のソースファイルに分割することは、ビルド時間を減らし、開発効率を高めるという利点もあります。

リンク 🔗

コンパイルによって生成されたオブジェクトファイルは、1つのプログラムの一部分のコードに過ぎず、単体では実行できません。これらを1つにまとめて、実行できる形式のファイル(実行ファイル)を生成する必要があります。この過程をリンク (link) と呼び、リンカ (linker) というソフトウェアによって行われます。

Visual Studio にはリンカも統合されています。

各オブジェクトファイルのコードに不備があれば、リンクエラー (link error) として報告されます。たとえば先ほどの例のように、関数を呼び出そうしているが、その定義がどのオブジェクトファイルにも含まれていないことが分かると、リンクエラーになります。

結合(リンケージ) 🔗

複数のソースファイルがあると、それぞれに同じ名前の定義が現れる可能性があります。

定義した名前がほかのソースファイルから使用できることを「外部結合(外部リンケージ) (external linkage) をもつ」と表現し、使用できないことを「内部結合(内部リンケージ) (internal linkage) をもつ」と表現します。また、関数内で定義された変数のように、ほかのソースファイルとの関わりがないものは、「無結合 (no linkage) である(リンケージをもたない)」といいます。

各種の定義がどの結合になるのかをまとめておきます。正確なルールはかなり複雑なので、ここでは現時点の知識の範囲のことに限っています。

【上級】関数は、static を付加していたり、無名名前空間(「スコープと名前空間」のページを参照)で宣言していたりする場合は内部結合になります。

【上級】データメンバは、static が付加されている場合は、所属するクラスの結合に合わせられます[4]

内部結合であれば、名前が同じだがコードに違いがある(初期値や型が違うなど)定義を、複数のソースファイルに記述できます。それぞれの定義は、まったく別のものであると認識されます。

外部結合であれば、ほかのソースファイルからでもその名前を使えますが、コードに違いがある定義を記述してはいけません。ポーカープログラムでは、構造体や列挙型の定義を2箇所に書いていますが、そのコードは完全に同じにしています。しかし、そもそも定義は1つに限るべきですし、必ず1つでなければならない場合もあります。定義を1つにする方法は、次のページで解説します。

外部結合では、ほかのソースファイルからでも名前が使えるといっても、それは名前だけの話であって、同じソースファイル内に定義したときのような無制限な利用ができることを意味しません。たとえば、ほかのソースファイルで定義している外部結合の関数を呼ぶには、宣言が必要です(ポーカープログラムの例では、関数の宣言を呼び出し側のソースファイルに記述しました)。同じ理屈で、ほかのソースファイルで定義されている構造体や列挙型の名前は使えますが、メンバや列挙子にアクセスするには定義が見えていなければなりません。

【上級】名前は使えるので、ほかのソースファイルで定義された外部結合の構造体や列挙型の宣言を記述することができます。

まとめ 🔗


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


参考リンク 🔗


練習問題 🔗

問題の難易度について。

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

問題1 (確認★)

複数のソースファイルに main関数の定義があるとどうなるか確認してみてください。

解答・解説

問題2 (確認★)

ポーカープログラムでは、2つのソースファイルのそれぞれに、以下の定義があります。

constexpr auto each_mark_card_num = 13;

もし、どちらか一方の定義だけ、型を long int型に変更したとすると、何が起こりますか?

解答・解説

問題3 (応用★★)

ポーカープログラムの init_trump関数や、get_mark_string関数は、ポーカー以外のトランプゲームでも流用できそうです。main.cpp から分離してみてください。

解答・解説

問題4 (応用★★)

ポーカープログラムの judge_poker_hand関数に、手札が整列されているかどうかのチェックを加えてください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




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