トラック
/
C++
C++
/
演習
/
データ博士
データ博士

データ博士

学習演習

はじめに

C++では、宣言と定義が分かれていることがよくあります。 宣言はヘッダーファイルと呼ばれるファイルにまとめ、それぞれの実装はソースファイルに書きます。 ヘッダーファイルは、APIだと考えてみるとわかりやすいでしょう。 ヘッダーファイルは、コードベースが_何_を提供しているかを教えてくれますが、_どのように_提供しているかの詳細には触れません。

ヘッダーとソース

ヘッダーファイルの拡張子として最もよく使われるのは.hです。 プロジェクトによっては.hppを使ったり、拡張子をまったく付けなかったりします。

定義は、別の.cppファイルに書きます。 この2つを結びつけるために、ソースファイルは対応するヘッダーファイルを_インクルード_することから始まります。

よく使う関数"super_root"を提供する"quick_math"というライブラリを書きたい場合、ファイルは次のようになります。

// A file named quick_math.h
#pragma once
namespace quick_math {
    double super_root(double x, int n);
}
// A file named quick_math.cpp
#include "quick_math.h"
#include <cmath>
double quick_math::super_root(double x, int n) {
    while(n) { x = std::sqrt(x), --n;}
    return x;
}

実装でだけ必要なヘッダーをインクルードする場合は、対応する#includeの行はソースファイルにだけ書けば済みます。 ヘッダーでインクルードしたものはすべて.cppファイルでも使えます。たとえば、下の例のstringライブラリのようにです。 注意: ヘッダーファイルでは宣言のあとに;が必要ですが、ソースファイルでは定義のあとに;は必要ありません。

Note

ExercismのC++の演習の多くは、ヘッダーとソースという2つのほぼ空のファイルから始まります。 演習を解くには、*_test.cppファイルを見て、期待されている関数の名前と名前空間を確認する必要があります。

クラスとヘッダー

クラスはとても複雑になることがあり、ヘッダーとソースの役割分担との関係はわかりにくく感じられるかもしれません。 1つのやり方として、実装の詳細はすべてソースファイルに置き、宣言とメンバー変数はすべてヘッダーに置くという構成があります。

// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H
#include <string>
namespace robots {
    class Flower {
        private:
            bool needs_water{};
            int size{};
            std::string name{};
        public:
            Flower(std::string name, int size = 0);
            void give_water();
            std::string get_name();
            int get_size();
            void start_next_day();
    };
}
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"
robots::Flower::Flower(std::string name, int size) {this->name = "Robotica " + name; this->size = size;}
void robots::Flower::start_next_day() {if (!needs_water) ++size; needs_water = true;}
std::string robots::Flower::get_name() {return name;}
int robots::Flower::get_size() {return size;}

ヘッダーをAPIの概要として使う場合、デフォルト値のような情報を探すのはそのヘッダーです。 そのため、コンストラクターのsize仮引数のデフォルト値は、実装ではなくヘッダーで扱います。 ソースファイルの定義には、名前空間robotsとクラス型Flowerを先頭に付けます。

もう1つの構成は、.cppファイルをまったく持たない、_ヘッダーのみ_のライブラリです。

// A file named robot_flower.h
#pragma once
#include <string>
namespace robots {
    class Flower {
        private:
            bool needs_water{};
            int size{};
            std::string name{};
        public:
            Flower(std::string name, int size = 0) {this->name = "Robotica " + name; this->size = size;}
            void give_water() {needs_water = false;}
            std::string get_name() {return name;}
            int get_size() {return size;}
            void start_next_day() {if (!needs_water) ++size; needs_water = true;}
    };
}

プロジェクトによってはこうした構成を組み合わせて使うこともあり、それぞれの用途にどれが最適かについては、さまざまな議論があります。

インクルードガード

上のヘッダーファイルの例にあった#pragma onceの行に気づいたかもしれません。 これはインクルードガードと呼ばれるもので、コンパイル時にファイルの内容が一度だけインクルードされるようにして、エラーを防ぎます。 インクルードガードには、#ifndefで始まり#endifで終わる、より複雑な書き方もあります。 役割は同じで、その使い方は上のFlowerクラスの例で示しています。

説明

オーストラリアにある「Doctor Data」という会社で、初出勤の日を迎えます。この会社は情報の復元を専門としています。 C++とフォン・ノイマン探査機の知識を武器に、面接は見事に突破しました。 テストファイルをたくさん見てきたので、新しい上司は、会社が最近復元したいくつかのテストコードから、対応するソースファイルとヘッダーファイルを再現するよう求めています。

この演習では、失われたファイルを再現します。 現時点では、ファイルの中身は使いものにならないゴミデータです。

Note

この概念演習の進め方は、Exercismの練習演習の構成とよく似ています。 演習の導入テキストは、仕様の一部にすぎません。 テストファイルこそが、問題を解くための確実な道しるべです。 C++のコンパイルの仕組み上、テストが必要とするすべてのクラス、関数、列挙型を、最小限のものでも実装し終えるまで、テスト結果がWebインターフェースに表示されないことがあります。 コンパイルエラーが出ても、すべてのタスクとテストに対応し終えれば消えることがあります。

タスクは4つあり、いずれもヘッダーファイルとソースファイルにある失われたコードの復元に関係しています。

1. まずは手がかりを探し出しましょう

テストファイルから復元された2行に目を向けます。

heaven::Vessel bob{"Robert Johansson", 1};
heaven::Vessel will{"Riker", 2, star_map::System::BetaHydri};

鋭い目ですぐにheavenという名前空間に気づきます。 Vesselというクラスが存在することもわかります。 コンストラクターは、どうやら2つか3つの引数で呼び出せるようです。 最初の引数はstring型のようで、2つ目は数値です。 Vesselクラスは、3つ目の引数でも初期化できます。 3つ目の引数は、star_map名前空間から来ています。 それはSystem型の列挙子です。 列挙型の1つ、BetaHydriまで手に入りました。

見つけた情報をもとに、ソースファイルとヘッダーファイルを用意しましょう。 必要な名前空間は2つ、heavenとstar_mapです。 heaven名前空間にはVesselクラスがあり、2つか3つの引数で呼び出せます。 System列挙型はstar_map名前空間に置き、BetaHydriという列挙子を持たせます。 他の列挙子にも目を光らせて、System列挙型をそのつど更新できるようにしましょう。

2. さらに詳しい情報を探しましょう

テストからさらに多くの行が見つかります。

heaven::Vessel bob{"Robert Johansson", 1};
REQUIRE(bob.current_system == star_map::System::Sol);
REQUIRE(bob.generation == 1);
heaven::Vessel bob5 = bob.replicate("Mario");
REQUIRE(bob5.current_system == star_map::System::Sol);
REQUIRE(bob5.generation == 2);

新たに見つかったテストの行から、Vesselクラスのもう1つのメンバー関数replicateが明らかになります。 この関数は文字列を受け取り、別のVesselインスタンスを返します。 前のタスクから、コンストラクターの3つ目の引数のデフォルト値の見当がつきます。 Vesselクラスの2つの公開メンバー変数と、replicateメンバー関数の仕様もわかります。

ヘッダーファイルとソースファイルに、replicate関数と公開メンバー変数current_system、generationを追加しましょう。

3. 内部の仕組みをのぞいてみましょう

復元されたテストファイルに、さらに興味深い行が見つかります。

heaven::Vessel bob6{"Homer", 3, star_map::System::EpsilonEridani};
REQUIRE(bob6.busters == 0);
bob6.make_buster();
REQUIRE(bob6.busters == 1);
bool success = bob6.shoot_buster();
REQUIRE(success);
REQUIRE(bob6.busters == 0);
success = bob6.shoot_buster();
REQUIRE_FALSE(success);

どうやらVesselクラスにはbustersというメンバー変数があり、2つのクラスメンバー関数make_busterとshoot_busterで変更できるようです。 他の情報が出てくるまでは、make_buster関数はvoidを返すと推測しておきます。 shoot_busterの戻り値に対するテストがあるので、この関数はboolを返すと仮定します。

Vesselクラスに2つの関数とメンバー変数を追加しましょう。 他のSystem列挙子も探し続けましょう。

4. 全体像を完成させましょう

テストファイルをひととおり見ても、まだ解明されていないコードの部分は2か所だけです。

heaven::Vessel bob1{"Bob", 1, star_map::System::AlphaCentauri};
heaven::Vessel marv{"Marvin", 2, star_map::System::DeltaEridani};
heaven::Vessel milo{"Milo", 3, star_map::System::DeltaEridani};
heaven::Vessel howie{"Howard", 4, star_map::System::Omicron2Eridani};

REQUIRE("Bob" == heaven::get_older_bob(bob1, marv));
REQUIRE(heaven::in_the_same_system(marv, milo));
REQUIRE_FALSE(heaven::in_the_same_system(marv, howie));

2つの関数が見えますが、これらはインスタンスから呼び出されていないので、Vesselクラスのメンバーではありません。 get_older_bobは2つのVesselインスタンスを比較し、stringを返します。 in_the_same_systemは2つのVesselインスタンスを比較し、boolを返します。

上で復元した行から、まだ足りない最後の関数を実装しましょう。

GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
C++ Exercism

データ博士を始める準備はできましたか?

Exercismに登録すれば、19個のコンセプト100個の演習、そして本物の人間によるメンタリングとともに、C++を学んでマスターできます。すべて無料です。