先頭へ戻る

分割コンパイル 解答ページ | Programming Place Plus 新C++編

Programming Place Plus トップページ新C++編分割コンパイル

先頭へ戻る

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (確認★)

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


以下のソースファイルを2つ用意して、ビルドしてみましょう。main関数の定義だけあれば十分です。

int main()
{
}

Visual Studio 2015 の場合、次のようなエラーが出ます。

fatal error LNK1169: 1 つ以上の複数回定義されているシンボルが見つかりました。

これはコンパイルエラーではなく、リンクエラーであることは重要です(エラー番号のところが “LNK” となっているのが、リンクエラーであることを示しています)。コンパイルはソースファイル1つずつに対して別個に行われるものであり、ほかのソースファイルはまったく見えていません。そのため、ほかのソースファイルに main関数の定義があっても問題ありません(同じソースファイル内での重複はコンパイルエラーです)(本編解説)。

問題2 (確認★)

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

constexpr auto each_mark_card_num = 13;

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


たとえば、main.cpp の側にある定義を次のように変更します。

constexpr long int each_mark_card_num = 13;

この状態でビルドしてみると、特に問題なく成功します。

同じ名前の定義なのに、型や初期値が異なることは、混乱を招きますし、もし両方の定義が見えている箇所から使おうとしたら、どちらの定義が有効とみなされるのかという問題があります。実際、そういう場合はエラーになるはずです。

しかし、関数の外側で定義された constexpr変数は内部結合です(本編解説)。内部結合の場合、ほかのソースファイルからはその名前が見えていません。そのため、そもそも同じ名前の定義があることを認識しておらず、両者はまったく別の存在であるため、型や初期値が異なっていても問題になりません。2つの定義が同じもののつもりでいるのはプログラマーだけなのです。

問題3 (応用★★)

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


card.cpp を追加して、そちらに移すことにします。Card型や each_mark_card_num、#include などいくつか追加が必要です。ソースファイル間でのコードの重複が激しくなりますが、この問題は次のページで解決を図るので、ここではやむを得ないものとして進めましょう。

// card.cpp

#include <string>
#include <vector>

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

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

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

constexpr auto each_mark_card_num = 13;                  // 各マークのカードの枚数

// トランプを初期化する
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;
    }
}

// カードのマークの文字列表現を返す
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 "";
    }
}

main.cpp の側からは、init_trump関数、get_mark_string関数の定義が消えますが、代わりに関数の宣言が必要です(本編解説)。

// main.cpp

#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;                        // 手札の枚数

// card.cpp にある関数の宣言
extern void init_trump(std::vector<Card>& cards);
extern std::string get_mark_string(CardMark card_mark);

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

// 手札を配る
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;
        }
    );
}

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";
}

問題4 (応用★★)

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


judge_poker_hand関数の実装は、引数で渡されてくる手札の情報が整列されていることを当てにしています。たとえば、ストレートの判定のとき、1枚目のカードの番号をみたあと、次のカードはその番号+1、その次のカードはさらに+1・・・という判定をしているので、整列されていないとうまくいきません。

整列済みであるかどうか調べるには、std::is_sorted関数を使うのが簡単です(「要素を整列する」のページを参照)。

judge_poker_hand関数は大きいので、冒頭部分だけ示します。

// 役を判定する
PokerHand judge_poker_hand(const std::vector<Card>& cards)
{
    if (cards.size() != hand_card_num) {
        return PokerHand::no_pair;
    }
    if (std::is_sorted(std::cbegin(cards), std::cend(cards),
        [](const Card& a, const Card& b){
            if (a.number == b.number) {
                return a.mark < b.mark;
            }
            return a.number < b.number;
        }) == false
    ) {
        return PokerHand::no_pair;
    }

    // 以下省略
}

ここでは単に「役無し」の戻り値を返しているだけですが、整列できていないミスに気付けるような方法が必要かもしれません。エラーメッセージを出力するなどでいいですが、このエラーは開発者の問題であって、遊ぶ人の問題ではないですから、本来は開発時にだけ分かる方法を採るのが適切です。

【上級】たとえば #if を使って、開発時にだけ有効になるコードを記述できます。

手札の整列ルールに合わせなければならないので、hand_out_cards関数の中にある std::sort関数に渡したラムダ式と同じ関数(ここではラムダ式)を与えなければなりません。まったく同じラムダ式を2か所に記述することになってしまうのは明らかによくありません。たとえば、手札の整列ルールはあとから変更されるかもしれず、忘れず、judge_poker_hand関数のほうも修正しなければならなくなります。別のソースファイルなので、これは非常に気付きにくいです。

ラムダ式の部分を関数として抜き出すのは1つの手です。問題③ で追加した card.cpp に置くといいでしょう。

// card.cpp

// カードの整列に使う比較関数
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;
}

そして、hand_out_cards関数や、judge_poker_hand関数を次のように変更します。cards_sort_compare関数の宣言も必要です。

// 手札を配る
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), cards_sort_compare);
}

// 役を判定する
PokerHand judge_poker_hand(const std::vector<Card>& cards)
{
    if (cards.size() != hand_card_num) {
        return PokerHand::no_pair;
    }
    if (std::is_sorted(std::cbegin(cards), std::cend(cards), cards_sort_compare) == false) {
        return PokerHand::no_pair;
    }

    // 以下省略
}

【上級】std::function や関数ポインタが使えるのなら、比較関数を引数で渡すように実装することができます。ほかのトランプゲームでの流用も考えると、そうした方を取ったほうがいいでしょう。


参考リンク



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー