C++入門|インターフェース

すべての力を見せる必要はない。インターフェースを使えば、相手に必要な機能だけを安全に公開できます。

C++でオブジェクト指向プログラミングを進めていくと、1つのクラスが複数の役割を持つことがあります。

たとえば、戦士を支援する多機能端末を考えてみましょう。

この端末には、遠く離れた修行場へ通信する機能と、修行用の音声案内を再生する機能が搭載されています。

通信担当の処理では、通信の開始と終了だけを使えれば十分です。
修行担当の処理では、音声案内の再生と停止だけを使えれば十分です。

ところが、多機能端末のクラスをそのまま渡すと、通信担当の処理から音声再生機能まで呼び出せてしまいます。

使う必要のない機能まで見えていると、間違った操作を書いてしまう可能性があります。

そこで役立つのがインターフェースです。

インターフェースを使うと、オブジェクトが持つ機能のうち、相手に必要な操作だけを見せられます。

ドラゴンボール風にたとえるなら、戦士支援端末には多くの機能がありますが、通信係には通信パネルだけを渡し、修行係には音声案内パネルだけを渡すような仕組みです。

端末そのものは同じでも、渡されたインターフェースによって、利用できる機能が変わります。

今回確認する内容は次のとおりです。

学習する内容役割
純粋仮想関数派生クラスで必ず実装する操作を定める
インターフェース利用できる操作の組み合わせを定める
多重継承1つのクラスで複数のインターフェースを実装する
アップキャスト派生クラスをインターフェース型として扱う
機能制限呼び出し側に必要な操作だけを公開する

C++におけるインターフェース

JavaやC#には、インターフェースを宣言するための専用機能があります。

一方、C++にはinterfaceという専用のキーワードはありません。

C++では、主な操作を純粋仮想関数として宣言した抽象基底クラスを使って、インターフェースに相当する設計を作ります。

純粋仮想関数は、次のように関数の宣言の末尾へ= 0を付けます。

virtual void sendSignal() = 0;

純粋仮想関数を持つクラスは抽象クラスになります。

抽象クラスから直接オブジェクトを作ることはできません。

class IKiCommunicator {
public:
    virtual void sendSignal() = 0;
};

次のようにIKiCommunicatorのオブジェクトを直接作ることはできません。

IKiCommunicator communicator;

IKiCommunicatorは、通信機能として何ができなければならないかを示す設計図です。

実際の処理は、このインターフェースを継承する派生クラスで実装します。

インターフェースが表すもの

インターフェースは、オブジェクトの内部構造ではなく、外部から利用できる操作を定めます。

たとえば、気力通信を行うインターフェースには、次の操作を用意します。

virtual void sendSignal(
    const std::string& destination) = 0;

virtual void closeSignal() = 0;

このインターフェースを実装するクラスは、通信開始と通信終了の処理を必ず用意しなければなりません。

どのような装置で通信するのか、内部でどのようなデータを使うのかは、インターフェース側では決めません。

インターフェースで定めるもの派生クラスで決めるもの
呼び出せる関数名関数内の具体的な処理
引数の型通信装置の内部構造
戻り値の型表示内容や制御方法
必要な操作の組み合わせデータの保存方法

ドラゴンボール風に考えると、インターフェースは技の使用規格です。

通信を開始する、通信を終了するという操作方法は共通ですが、神殿の通信装置で実行するのか、宇宙船の通信装置で実行するのかは、それぞれのクラスが決めます。

図:インターフェースが機能の境界を作る

この図が示していること

WarriorSupportDeviceは、通信機能と修行音声機能の両方を持っています。

IKiCommunicatorとして扱う場合は、通信に関する操作だけが見えます。

ITrainingPlayerとして扱う場合は、修行音声に関する操作だけが見えます。

同じオブジェクトでも、どのインターフェースを通して利用するかによって、呼び出せる機能が制限されます。

今回作成するクラス

今回のプログラムでは、2つのインターフェースと1つの具体的なクラスを作成します。

クラス種類役割
IKiCommunicatorインターフェース気力通信の開始と終了を定める
ITrainingPlayerインターフェース修行音声の再生と停止を定める
WarriorSupportDevice具体的なクラス2つのインターフェースを実装する

WarriorSupportDeviceは、IKiCommunicatorとITrainingPlayerの両方を継承します。

class WarriorSupportDevice
    : public IKiCommunicator,
      public ITrainingPlayer {
};

C++では、1つのクラスが複数の親クラスを継承できます。

これを多重継承と呼びます。

今回のように、実装を持たないインターフェースを複数継承する設計は、C++で複数の役割を表現するときによく使われます。

気力通信インターフェースを作る

最初に、気力通信の操作を定めるIKiCommunicatorを作ります。

このインターフェースには、通信開始と通信終了の純粋仮想関数を用意します。

プロジェクト/ファイル名: Chap7_06/iki_communicator.h

#ifndef CHAP7_06_IKI_COMMUNICATOR_H
#define CHAP7_06_IKI_COMMUNICATOR_H

#include <string>

// 気力通信機能を表すインターフェース
class IKiCommunicator {
public:
    // 基底クラスのポインタから安全に破棄できるようにする
    virtual ~IKiCommunicator() = default;

    // 指定された修行場へ通信を開始する
    virtual void sendSignal(
        const std::string& destination) = 0;

    // 気力通信を終了する
    virtual void closeSignal() = 0;
};

#endif

IKiCommunicatorには、通常のメンバ変数や具体的な通信処理を用意していません。

外部から利用できる通信操作だけを宣言しています。

virtual void sendSignal(
    const std::string& destination) = 0;

virtual void closeSignal() = 0;

末尾に= 0が付いているため、どちらも純粋仮想関数です。

IKiCommunicatorを継承する具体的なクラスは、この2つを実装する必要があります。

仮想デストラクタを用意する理由

インターフェースには、仮想デストラクタも用意しています。

virtual ~IKiCommunicator() = default;

インターフェース型のポインタを使って派生クラスのオブジェクトを削除する可能性がある場合、基底クラスのデストラクタをvirtualにしておく必要があります。

今回のmain関数ではスタック上にオブジェクトを作るため、インターフェース型のポインタからdeleteは行いません。

それでも、インターフェースとして再利用しやすい安全な設計にするため、仮想デストラクタを用意しています。

defaultは、デストラクタの標準的な処理をコンパイラに生成してもらう指定です。

修行音声インターフェースを作る

次に、修行用の音声案内を操作するITrainingPlayerを作ります。

プロジェクト/ファイル名: Chap7_06/itraining_player.h

#ifndef CHAP7_06_ITRAINING_PLAYER_H
#define CHAP7_06_ITRAINING_PLAYER_H

// 修行音声機能を表すインターフェース
class ITrainingPlayer {
public:
    // 基底クラスのポインタから安全に破棄できるようにする
    virtual ~ITrainingPlayer() = default;

    // 修行用の音声案内を再生する
    virtual void playGuide() = 0;

    // 修行用の音声案内を停止する
    virtual void stopGuide() = 0;
};

#endif

ITrainingPlayerには、playGuideとstopGuideがあります。

どちらも純粋仮想関数なので、ITrainingPlayerだけでは具体的な音声再生は行えません。

実際の処理は、ITrainingPlayerを継承するクラスで実装します。

インターフェース名に付けたI

今回のクラス名は、IKiCommunicatorとITrainingPlayerです。

先頭のIは、インターフェースであることを分かりやすくするための命名方法です。

C++の文法上、Iを付ける決まりはありません。

次のような名前でも文法上は問題ありません。

class KiCommunicator

ただし、Iを付けることで、具体的な装置クラスではなく、操作の規格を表す型だと判断しやすくなります。

名前意味
IKiCommunicator気力通信機能のインターフェース
ITrainingPlayer修行音声機能のインターフェース
WarriorSupportDevice実際に機能を実装するクラス

2つのインターフェースを継承する

WarriorSupportDeviceは、気力通信と修行音声の両方に対応する多機能端末です。

そのため、IKiCommunicatorとITrainingPlayerを多重継承します。

プロジェクト/ファイル名: Chap7_06/warrior_support_device.h

#ifndef CHAP7_06_WARRIOR_SUPPORT_DEVICE_H
#define CHAP7_06_WARRIOR_SUPPORT_DEVICE_H

#include <string>
#include "iki_communicator.h"
#include "itraining_player.h"

// 戦士を支援する多機能端末
class WarriorSupportDevice
    : public IKiCommunicator,
      public ITrainingPlayer {

public:
    // 気力通信を開始する
    void sendSignal(
        const std::string& destination) override;

    // 気力通信を終了する
    void closeSignal() override;

    // 修行用の音声案内を再生する
    void playGuide() override;

    // 修行用の音声案内を停止する
    void stopGuide() override;
};

#endif

クラス名のあとにコロンを書き、2つのインターフェースをpublic継承しています。

class WarriorSupportDevice
    : public IKiCommunicator,
      public ITrainingPlayer

これによって、WarriorSupportDeviceは気力通信端末としても、修行音声プレーヤーとしても扱えるようになります。

overrideで実装を明確にする

WarriorSupportDeviceのメンバ関数には、overrideを付けています。

void playGuide() override;

overrideは、この関数が基底クラスの仮想関数をオーバーライドしていることを表します。

関数名や引数の型を間違えた場合は、コンパイラがエラーを出します。

たとえば、インターフェース側が次の宣言だとします。

virtual void playGuide() = 0;

派生クラスで間違えて次のように書くと、同じ関数を実装したことにはなりません。

void playGuides() override;

名前が異なるため、overrideによってコンパイルエラーとして発見できます。

指定役割
virtual仮想関数として宣言する
= 0純粋仮想関数にする
override基底クラスの仮想関数を実装していることを示す

図:多重継承によって2つの役割を実装する

この図が示していること

WarriorSupportDeviceは、IKiCommunicatorとITrainingPlayerを多重継承しています。

そのため、気力通信に必要な2つの関数と、修行音声に必要な2つの関数をすべて実装します。

IKiCommunicatorとして見ると通信装置になり、ITrainingPlayerとして見ると修行音声プレーヤーになります。

具体的な処理を実装する

WarriorSupportDeviceのメンバ関数をcppファイルへ実装します。

プロジェクト/ファイル名: Chap7_06/warrior_support_device.cpp

#include "warrior_support_device.h"
#include <iostream>

using namespace std;

// 指定された修行場へ気力通信を開始する
void WarriorSupportDevice::sendSignal(
    const string& destination) {

    cout << destination
         << "へ気力通信を開始しました。"
         << endl;
}

// 気力通信を終了する
void WarriorSupportDevice::closeSignal() {
    cout << "気力通信を終了しました。" << endl;
}

// 修行用の音声案内を再生する
void WarriorSupportDevice::playGuide() {
    cout << "重力修行の音声案内を再生しました。"
         << endl;
}

// 修行用の音声案内を停止する
void WarriorSupportDevice::stopGuide() {
    cout << "修行音声の再生を停止しました。"
         << endl;
}

sendSignalは、通信先を引数として受け取ります。

void WarriorSupportDevice::sendSignal(
    const string& destination)

destinationは読み取りだけなので、const参照で受け取っています。

関数内では、受け取った通信先を表示します。

cout << destination
     << "へ気力通信を開始しました。"
     << endl;

残りの3つの関数も、それぞれの役割に合ったメッセージを表示します。

すべての純粋仮想関数を実装する

WarriorSupportDeviceは、2つのインターフェースを継承しています。

そのため、次の4つの純粋仮想関数をすべて実装しなければなりません。

void sendSignal(
    const std::string& destination) override;

void closeSignal() override;
void playGuide() override;
void stopGuide() override;

たとえば、stopGuideの実装を忘れた場合、WarriorSupportDeviceには未実装の純粋仮想関数が残ります。

その場合、WarriorSupportDeviceも抽象クラスのままです。

次のようにオブジェクトを作ろうとすると、コンパイルエラーになります。

WarriorSupportDevice device;

具体的なオブジェクトを作るには、継承した純粋仮想関数をすべて実装する必要があります。

インターフェース型の関数を作る

main.cppでは、通信機能を利用するuseCommunicatorと、修行音声を利用するuseTrainingPlayerを作ります。

useCommunicatorは、IKiCommunicator型のポインタを受け取ります。

void useCommunicator(
    IKiCommunicator* communicator)

useTrainingPlayerは、ITrainingPlayer型のポインタを受け取ります。

void useTrainingPlayer(
    ITrainingPlayer* player)

この引数の型によって、各関数で利用できる機能が制限されます。

プロジェクト/ファイル名: Chap7_06/main.cpp

#include <iostream>
#include "warrior_support_device.h"

using namespace std;

// 気力通信機能だけを利用する
void useCommunicator(
    IKiCommunicator* communicator);

// 修行音声機能だけを利用する
void useTrainingPlayer(
    ITrainingPlayer* player);

int main(int argc, char** argv) {
    // 通信と修行音声の両方を持つ端末
    WarriorSupportDevice device;

    // IKiCommunicatorとして渡す
    useCommunicator(&device);

    cout << endl;

    // ITrainingPlayerとして渡す
    useTrainingPlayer(&device);

    return 0;
}

// 気力通信機能だけを利用する
void useCommunicator(
    IKiCommunicator* communicator) {

    cout << "=== 気力通信担当 ===" << endl;

    communicator->sendSignal("北の界王流修行場");
    communicator->closeSignal();

    // IKiCommunicatorには定義されていないため呼び出せない
    // communicator->playGuide();
    // communicator->stopGuide();

    cout << "====================" << endl;
}

// 修行音声機能だけを利用する
void useTrainingPlayer(
    ITrainingPlayer* player) {

    cout << "=== 修行音声担当 ===" << endl;

    player->playGuide();
    player->stopGuide();

    // ITrainingPlayerには定義されていないため呼び出せない
    // player->sendSignal("神殿");
    // player->closeSignal();

    cout << "====================" << endl;
}

実行結果

=== 気力通信担当 ===
北の界王流修行場へ気力通信を開始しました。
気力通信を終了しました。
====================

=== 修行音声担当 ===
重力修行の音声案内を再生しました。
修行音声の再生を停止しました。
====================

同じオブジェクトを異なる型として渡す

main関数で作っているオブジェクトは1つです。

WarriorSupportDevice device;

このdeviceは、通信機能と修行音声機能の両方を持っています。

最初に、useCommunicatorへ渡しています。

useCommunicator(&device);

useCommunicatorが受け取る型はIKiCommunicator*です。

WarriorSupportDeviceはIKiCommunicatorをpublic継承しているため、WarriorSupportDeviceからIKiCommunicatorへ変換できます。

次に、同じdeviceをuseTrainingPlayerへ渡しています。

useTrainingPlayer(&device);

今度は、WarriorSupportDeviceからITrainingPlayerへ変換されます。

渡すオブジェクト受け取る型関数内で使える機能
deviceIKiCommunicator*通信開始、通信終了
deviceITrainingPlayer*音声再生、音声停止

実際のオブジェクトは同じですが、受け取るインターフェース型によって、利用できる操作が変わります。

アップキャスト

派生クラス型のポインタを、基底クラス型のポインタとして扱う変換をアップキャストと呼びます。

今回の関係は次のとおりです。

IKiCommunicator
       ↑
WarriorSupportDevice

WarriorSupportDeviceはIKiCommunicatorを継承しているため、WarriorSupportDeviceをIKiCommunicatorとして扱えます。

同じように、ITrainingPlayerへもアップキャストできます。

ITrainingPlayer
       ↑
WarriorSupportDevice

public継承によるアップキャストは、安全な変換として暗黙に行えます。

そのため、次の呼び出しに明示的なキャストは必要ありません。

useCommunicator(&device);
useTrainingPlayer(&device);

明示的にアップキャストする場合

型変換を明示したい場合は、static_castを使えます。

useCommunicator(
    static_cast<IKiCommunicator*>(&device));

useTrainingPlayer(
    static_cast<ITrainingPlayer*>(&device));

ただし、今回の変換はコンパイラが安全に判断できるため、通常は暗黙のアップキャストで十分です。

C言語形式のキャストも書けますが、C++ではstatic_castなど、目的が明確なキャストを使うほうが安全です。

書き方特徴
useCommunicator(&device)暗黙のアップキャスト
static_cast<IKiCommunicator*>(&device)意図を明示したアップキャスト
(IKiCommunicator*)&deviceC言語形式で、意図が分かりにくい

関係のないクラス同士を、安全なアップキャストとして変換することはできません。

インターフェースで利用できる機能が決まる

useCommunicatorの引数はIKiCommunicator*です。

void useCommunicator(
    IKiCommunicator* communicator)

そのため、関数内で利用できるのは、IKiCommunicatorに宣言された関数です。

communicator->sendSignal(
    "北の界王流修行場");

communicator->closeSignal();

WarriorSupportDeviceにはplayGuideとstopGuideもあります。

しかし、communicatorの型はIKiCommunicator*なので、修行音声機能は見えません。

communicator->playGuide();

この処理はコンパイルエラーになります。

IKiCommunicatorにはplayGuideというメンバ関数が定義されていないからです。

修行音声側でも機能が制限される

useTrainingPlayerの引数はITrainingPlayer*です。

void useTrainingPlayer(
    ITrainingPlayer* player)

この関数では、playGuideとstopGuideを呼び出せます。

player->playGuide();
player->stopGuide();

一方、通信機能は呼び出せません。

player->sendSignal("神殿");

ITrainingPlayerにはsendSignalがないため、コンパイルエラーになります。

この制限は、実行中に行われるものではありません。

コードをコンパイルした時点で、許可されていない操作を発見できます。

図:インターフェースによる機能制限

この図が示していること

WarriorSupportDeviceは4つの機能を持っています。

useCommunicatorへ渡すと、IKiCommunicatorを通して通信機能だけが公開されます。

useTrainingPlayerへ渡すと、ITrainingPlayerを通して修行音声機能だけが公開されます。

必要のない機能は型によって見えなくなるため、誤った操作をコードへ書きにくくなります。

インターフェースを使わずに渡した場合

useCommunicatorがWarriorSupportDevice*をそのまま受け取る設計を考えてみましょう。

void useCommunicator(
    WarriorSupportDevice* device)

この場合、関数内から4つの機能をすべて呼び出せます。

device->sendSignal("神殿");
device->closeSignal();
device->playGuide();
device->stopGuide();

通信担当の処理に修行音声機能は必要ありません。

それでも呼び出せるため、間違ってplayGuideを実行するコードを書く可能性があります。

渡し方関数内で見える機能
WarriorSupportDevice*端末が持つすべての機能
IKiCommunicator*通信機能だけ
ITrainingPlayer*修行音声機能だけ

インターフェースを使うと、利用側が必要とする最小限の操作だけを公開できます。

インターフェースによる制限とprivateの違い

インターフェースによる機能制限は、privateによるアクセス制御とは異なります。

WarriorSupportDeviceでは、4つの関数をpublicとして実装しています。

void sendSignal(...) override;
void closeSignal() override;
void playGuide() override;
void stopGuide() override;

WarriorSupportDevice型として扱えば、4つすべてを呼び出せます。

IKiCommunicator型として扱うと、通信機能だけが型の上で見えるようになります。

つまり、関数そのものをprivateにするのではなく、どの型を相手へ渡すかによって、見せる機能を選んでいます。

インターフェースを使うメリット

インターフェースを使う主なメリットは、必要な機能だけを相手へ提供できることです。

メリット内容
機能を制限できる不要なメンバ関数を呼び出せなくする
誤操作を防ぎやすい間違った呼び出しをコンパイル時に発見できる
役割を分離できる通信処理と音声処理を別々に設計できる
実装を交換しやすい同じインターフェースを実装する別クラスへ変更できる
依存を小さくできる利用側が具体的なクラスの詳細を知らなくてよい

ドラゴンボール風に考えると、通信担当には通信の技だけを許可し、修行担当には音声案内の技だけを許可する仕組みです。

誰にどの力を使わせるのかを、型によって安全に管理できます。

具体的なクラスに依存しない設計

useCommunicatorは、WarriorSupportDeviceそのものを要求していません。

要求しているのはIKiCommunicatorを実装したオブジェクトです。

void useCommunicator(
    IKiCommunicator* communicator)

そのため、別の通信装置クラスを作った場合でも、IKiCommunicatorを実装していればuseCommunicatorへ渡せます。

class TempleCommunicator
    : public IKiCommunicator {
public:
    void sendSignal(
        const std::string& destination) override;

    void closeSignal() override;
};

useCommunicator側の処理を変更する必要はありません。

TempleCommunicator templeDevice;
useCommunicator(&templeDevice);

利用側は、実際の装置がWarriorSupportDeviceなのかTempleCommunicatorなのかを詳しく知る必要がありません。

IKiCommunicatorとして必要な関数を呼び出すだけです。

インターフェースとポリモーフィズム

インターフェース型のポインタから純粋仮想関数を呼び出すと、実際のオブジェクトが実装した関数が実行されます。

communicator->sendSignal(
    "北の界王流修行場");

communicatorの型はIKiCommunicator*ですが、実際に指しているオブジェクトはWarriorSupportDeviceです。

そのため、WarriorSupportDevice::sendSignalが実行されます。

このように、共通の基底型を通して、実際のオブジェクトに応じた処理を呼び出す仕組みがポリモーフィズムです。

今回のインターフェースは、このポリモーフィズムを利用しています。

ポインタではなく参照でも受け取れる

インターフェースは、ポインタだけでなく参照でも受け取れます。

void useCommunicator(
    IKiCommunicator& communicator) {

    communicator.sendSignal(
        "北の界王流修行場");

    communicator.closeSignal();
}

呼び出し側では、オブジェクトをそのまま渡します。

WarriorSupportDevice device;
useCommunicator(device);
受け取り方呼び出しメンバアクセス
IKiCommunicator*useCommunicator(&device)communicator->sendSignal
IKiCommunicator&useCommunicator(device)communicator.sendSignal

対象のオブジェクトが必ず存在する場合は、参照で受け取る設計も分かりやすい方法です。

nullptrを表現する必要がある場合は、ポインタが向いています。

インターフェースを設計するときの注意点

インターフェースには、その役割に必要な操作だけを含めます。

IKiCommunicatorへ修行音声の関数まで追加すると、通信機能と音声機能を分けた意味が弱くなります。

class IKiCommunicator {
public:
    virtual void sendSignal(...) = 0;
    virtual void closeSignal() = 0;

    // 通信の役割ではない
    virtual void playGuide() = 0;
};

通信と音声を分離するなら、それぞれ別のインターフェースにします。

インターフェース含める操作
IKiCommunicator通信開始、通信終了
ITrainingPlayer音声再生、音声停止

インターフェースを細かい役割に分けることで、利用側へ必要な機能だけを渡しやすくなります。

インターフェース型から具体的な型へ戻す場合

インターフェース型のポインタを、具体的な派生クラス型へ変換する処理をダウンキャストと呼びます。

IKiCommunicator* communicator = &device;

このcommunicatorをWarriorSupportDevice*へ戻したい場合があります。

ポリモーフィックな基底クラスから安全性を確認して変換する場合は、dynamic_castを使えます。

WarriorSupportDevice* devicePointer =
    dynamic_cast<WarriorSupportDevice*>(
        communicator);

変換できなかった場合はnullptrになります。

ただし、インターフェースを使う目的は、具体的なクラスの詳細へ依存しないことです。

利用側で何度も具体的な型へ戻す必要があるなら、インターフェースの分け方や関数の設計を見直したほうがよい場合があります。

インターフェースが有効な開発場面

大きなシステムでは、複数の担当者が別々の機能を開発します。

たとえば、次のように役割を分担できます。

担当利用するインターフェース
気力通信担当IKiCommunicator
修行案内担当ITrainingPlayer
端末開発担当WarriorSupportDevice
神殿通信装置担当TempleCommunicator

気力通信担当は、IKiCommunicatorに定義された操作だけを使います。

端末の内部構造や、修行音声の仕組みを知る必要はありません。

修行案内担当も、ITrainingPlayerだけを使います。

このように役割を分離すると、別の担当が作った機能を誤って操作しにくくなります。

また、具体的な装置が変更されても、インターフェースが同じなら利用側のコードを変更せずに済む可能性があります。

インターフェースで身につけたい感覚

C++にはinterfaceという専用キーワードはありません。

純粋仮想関数を持つ抽象基底クラスを使って、インターフェースに相当する設計を作ります。

class IKiCommunicator {
public:
    virtual ~IKiCommunicator() = default;

    virtual void sendSignal(
        const std::string& destination) = 0;

    virtual void closeSignal() = 0;
};

具体的なクラスは、インターフェースを継承し、純粋仮想関数を実装します。

class WarriorSupportDevice
    : public IKiCommunicator,
      public ITrainingPlayer {
};

同じWarriorSupportDeviceオブジェクトでも、IKiCommunicatorとして渡せば通信機能だけが見えます。

useCommunicator(&device);

ITrainingPlayerとして渡せば、修行音声機能だけが見えます。

useTrainingPlayer(&device);

ドラゴンボール風にたとえるなら、多機能な戦士支援端末を丸ごと渡すのではなく、担当ごとに専用の操作パネルを渡す仕組みです。

通信担当へ渡すパネルには通信ボタンしかありません。
修行担当へ渡すパネルには音声再生ボタンしかありません。

相手に必要な機能だけを見せることで、誤操作を減らし、それぞれの役割を分かりやすくできます。

インターフェースは、単に関数を制限するだけの仕組みではありません。

具体的なクラスの内部構造から利用側を切り離し、共通の操作規格によって複数のクラスを扱えるようにする設計方法です。

純粋仮想関数、多重継承、アップキャスト、ポリモーフィズムを組み合わせることで、必要な機能だけを安全に公開するC++プログラムを作れるようになります。

サンプルでは、不要なC言語形式のキャストを使わず、安全な暗黙のアップキャストを利用する構成にしています。