LoginSignup
95
107

More than 5 years have passed since last update.

C++入門者に贈るclass入門とclass/structキーワードの使い分け

Last updated at Posted at 2017-06-10

注意

この記事にはtemplateを利用したサンプルコードや技法が紹介されている場所があります。入門者は卒業した!という方以外はスルーしてください。また
C99からC++14を駆け抜けるC++講座
も合わせてお読みください。

また、この記事はC++入門者がこの記事をんだだけで内容を理解できることを目的としておらず調べるきっかけを与えることを意図したものです。当然わからない単語や機能がいきなり出てきます

はじめに

みなさま、ナマステ
さて、この記事を読んでいる方ならC言語の構造体についてはすでにご存知だと思います。

C99
typedef struct {
  float x;
  float y;
} Point;

こんな感じのやつですね。

ところでC++にはなんかClassとかいうのがあるらしい。なんか関数が持てるとかきいたぞ?オブジェクト指向が云々とか誰かが言ってた!

Classとオブジェクト指向は別物です

まあそもそもC++のオブジェクト指向はオブジェクト指向じゃないみたいな話もありますがそれはさておき、Classとオブジェクト指向は別物ですといっておく必要はあるでしょう。

どういうことかというと、

# C++でオブジェクト指向プログラミングをするためにほしい言語機能
- Class
- etc
.
.
.

であって、

Class == オブジェクト指向

ではないということです。実際このあと紹介しますがClassはオブジェクト指向プログラミング以外にも多様な用途があります。

Classとは

めったに使われない機能やC++入門者に説明しづらい機能は省略します。

Cの構造体宣言でタグ名とか言われていた部分がクラス名になります。なのでいちいちtypedefする必要はありません。

C++
struct Hoge {};
class Fuga {};

Cの構造体とおなじく(非staticな)メンバ変数を持てる

C++
class Point {
  float x_;
  float y_;
};

まあそりゃそうですね。

constメンバ変数を持てる

忘れろ

アクセス指定がある

private, protected, public の三種類あります。structキーワードでクラスを宣言したときはデフォルトでpublicclassキーワードでクラスを宣言したときはデフォルトでprivateになります。

なにが嬉しいかは後述します。

型定義をメンバに持てる

C++
class Point {
  float x_;
  float y_;
public:
  using value_type = float;
};
int main()
{
  Point::value_type a = 3.1f;
}

クラスの中だけで有効な型aliasです。ぶっちゃけtemplateと組み合わせないとほとんど有り難みがないです。アクセス指定がpublicのときは、クラスの外から::演算子で利用できますが、C++11で追加されたauto型推論とかtemlate alias(解説しません)のせいでめったに::演算子をクラスの外から型aliasを使うために使うことはなくなりました。

ちなみにusing value_type = float;という記法はusing aliasというC++11で追加されたtypedefに変わる記法です。typedef は C++11 ではオワコンらしいので積極的に使っていきましょう。

(非staticで非virtualな)メンバ関数を持てる

お待たせしました、「C++のクラスは関数が持てるんだよ~」というあれです。え? static って? virtual ってなんだって?後述しますが、簡単に言うとふつうのメンバ関数です。

C++
#include <iostream>
class Point {
public:
  float x_;
  float y_;
  float x() const noexcept { return this->x_; }
  float& x() noexcept { return this->x_; }
  float y() const noexcept { return this->y_; }
  float& y() noexcept { return this->y_; }
};
int main()
{
  Point p = {};
  std::cout << p.x() << std::endl;
}

メンバ関数からメンバ変数にアクセスするには、thisという、自分自身のクラスを指す特殊なポインタを利用します。これは暗黙のうちにメンバ関数を呼び出すときに0番目の引数として渡されています。

C++の場合thisを渡すのが関数呼び出し規約レベル(thiscallとか)でやっているのでピンとこないかもしれませんが、例えばRustという言語の場合

Rust
struct A;

// parse_self_arg
impl A {
    fn f1(self: A) {}
    fn f2(self: &mut A) {}
    fn f3(self: &A) {}
    fn f4(self: Box<A>) {}
    // 生存期間を明示すると以下の通り
    // fn f2<'a>(self: &'a mut A) {}
    // fn f3<'a>(self: &'a A) {}
}

はっきりと明示的にselfを引数に指定して宣言します(selfってのがC++でいうthisに該当する)。
ref: Rustのself引数まとめ - 簡潔なQ

luaという言語なら

Lua
-- ################# define function
-- オブジェクトを作成するnewメソッドを定義(Javascriptで言うコンストラクタのようなもの)
Coordinate = {}
Coordinate.new = function(_x,_y)
  local obj = {}
  obj.x = _x
  obj.y = _y

  -- pp というインスタンスメソッドをオブジェクト内に格納
  obj.pp = function(self) print(string.format("(%d,%d)", self.x, self.y)) end

  return obj 
end

-- ################## main
point1 = Coordinate.new(1,3) -- 座標(1,3)というオブジェクト
point2 = Coordinate.new(3,2) -- 座標(3,2)というオブジェクト

point1.pp(point1)
point2.pp(point2)

呼び出しすら明示的にメソッドの第一引数に指定します(もっとも面倒なのでシンタックスシュガーとして point1.pp(point1)point1:pp()と書けるようにはなっています)。
ref: Luaでオブジェクト指向(1)―基本はコロン記法とメタテーブル - Minecraftとタートルと僕

話をC++に戻しましょう。p.x()のようにしてメンバ関数を呼び出せますが、この時pへのポインタが0番目の引数としてメンバ関数Point::xに渡されます。この0番目の引数がthisなんですね。

ちなみにthisはNullポインタにならないんだからなんで参照やないや!と思うかもしれませんが、thisがポインタであるのは、単にthisが参照よりも早く導入されたという、歴史的な理由でしかないです、あきらめましょう。

メンバ関数は、thisポインタがconst修飾か否かでoverloadできます

C++
#include <iostream>
class Point {
public:
  float x_;
  float y_;
  float x() const noexcept { return this->x_; }// -- (1)
  float& x() noexcept { return this->x_; }// -- (2)
  float y() const noexcept { return this->y_; }
  float& y() noexcept { return this->y_; }
};
void use_const_version(const Point& p) {
  //p.x() = 3;//const修飾されていないメンバ関数は呼び出せない
  std::cout << p.x() << std::endl;//(1)の方が呼ばれる
}
void use_non_const_version(Point& p) {
  p.x() = 3;//(2)の方が呼ばれる。p.x_へのlvalue referenceが返されされるので書き換えられる
  std::cout << p.x() << std::endl;//(2)の方が呼ばれる
}
int main()
{
  Point p = {};
  use_const_version(p);// => 0
  use_non_const_version(p);// => 3
  use_const_version(p);// => 3
}

ちなみにメンバ関数の実態はクラスに固有なのでオブジェクト作るごとに関数の実体が増殖したりはしません(じゃないとデータ実行防止(DEP)が使えなくて危ない)。

コンストラクタをもつ

まず、Cでいう構造体やC++のクラス型の変数をオブジェクトと言うことがあります

で、オブジェクトをどのように生成するかを決めるのがコンストラクタです。基本的にはメンバ変数の初期化をするためのものですが、べつにそれ以外のこともできます。呼び出し方とできることが特殊なメンバ関数ですからね。

C++
#include <cstddef>
class inferior_string_view {
public:
  using size_type = std::size_t;
private:
  char* begin_;
  size_type n_;
public:
  inferior_string_view(char* begin, size_type size) noexcept : begin_(begin), n_(size) {}
  char front() const { return *(this->begin_); }
}

クラス名(引数opt) noexceptとかopt : メンバー初期化子opt { コンストラクタの処理}

のように書きます。宣言と定義を分けたいときは

クラス名(引数opt) noexceptとかopt;

と宣言して、

クラス名::クラス名(引数opt) noexceptとかopt : メンバー初期化子opt { コンストラクタの処理}

と定義します。

メンバ初期化子は

  • メンバ変数名(値): 対象のメンバ変数を明示的初期化する
  • メンバ変数名{値}: 対象のメンバ変数を明示的初期化する
  • メンバ変数名(): 対象のメンバ変数を値初期化する
  • メンバ変数名{}: 対象のメンバ変数を値初期化する
  • クラス名(値): 自分のクラスのほかのコンストラクタを呼び出す
  • 基底クラス名(値): 継承をしているときの基底クラス(派生元, base class, 解説しません)のコンストラクタを呼び出す

のように書きます

メンバ初期化子は必ずメンバー変数の宣言順に書きましょう。基本的にメンバ変数の初期化はメンバ初期化子でやりますが、初期化順序に依存性がある場合はコンストラクタの処理の中に書きましょう。もちろんコンストラクタの処理ではthisが使えます。

明示的初期化や値初期化については極めて難解ですが、基本的には

  • 組み込み型(int, double, etc...): =初期化と同じ結果
  • オブジェクト: コンストラクタを呼び出す

という挙動だと思ってください。詳細は

に譲りますが、頭がこんがらがるだけなので理解しないほうが幸せかと思います。

(非staticな)メンバ変数にデフォルト値を設定できる

メンバ変数のデフォルト初期化という機能です。コンストラクタで初期化するオブジェクトですが、コンストラクタは何個も書くことが普通なため、メンバ変数の初期化忘れをする可能性があります。そこで登場するのがこの機能で、コンストラクタで初期化忘れしても、設定したデフォルト値で初期化されます。もちろんコンストラクタの初期化が優先されます。

C++
#include <iostream>
class Hoge {
  int a = 3;
};
int main()
{
  Hoge hoge;
  std::cout << hoge.a << std::endl;// => 3
}

operator overloadできる

さて、C++にはoperator overloadという言語機能があります。これはオブジェクトに対する演算子の挙動を定義できる機能です(ただしallocation functionとdeallocation functionは除く)。後述する例外を除き勝手にオブジェクトに対する演算子を作ってはくれません。

operator overloadの種類としては

  1. 普通のoperator overload
  2. 型変換演算子
  3. allocation function
  4. deallocation function
  5. User-defined literal

がありますが、2はbool型へのexplict conversion operator(解説しません)以外忘れてください。3,4は忘れてください。5はここでは解説しません。

普通のoperator overloadとは、+ - * / % ˆ & | ~ ! = < > += -= *= /= %= ˆ= &= |= << >> >>= <<= == != <= >= && || ++ -- , ->* -> () []演算子をオブジェクトに対して定義することです。

  • クラス内メンバ関数としてのみ定義できるoperator
  • クラス外でも定義できるoperator
  • クラス外でしか定義できないoperator

があります。

クラス内メンバ関数としてのみ定義できるoperatorは、=(assign) ->(arrow) ()(function call) []です。

と、難しい話はこの辺にしてoperator overloadの実例を見ましょう。

C++
#include <iostream>
#include <cassert>
class Point {
  float x_;
  float y_;
public:
  using value_type = float;
  Point(float x, float y) noexcept : x_(x), y_(y) {}
  float x() const noexcept { return this->x_; }
  float& x() noexcept { return this->x_; }
  float y() const noexcept { return this->y_; }
  float& y() noexcept { return this->y_; }
  float operator[](std::size_t n) const noexcept
  {
    assert(2 <= n);
    return (0 == n) ? this->x_ : this->y_;
  }
  float& operator[](std::size_t n) noexcept
  {
    assert(2 <= n);
    return (0 == n) ? this->x_ : this->y_;
  }
};
bool operator==(const Point& l, const Point& r) noexcept { return l.x() == r.x() && l.y() == r.y(); }
bool operator==(const Point& l, std::nullptr_t) noexcept { return l.x() == 0 && l.y() == 0; }
bool operator==(std::nullptr_t, const Point& r) noexcept { return r.x() == 0 && r.y() == 0; }
bool operator!=(const Point& l, const Point& r) noexcept { return !(l == r); }
bool operator!=(const Point& l, std::nullptr_t) noexcept { return !(l == 0); }
bool operator!=(std::nullptr_t, const Point& r) noexcept { return !(0 == r); }
int main()
{
  Point p1 = {};
  Point p2 = {};
  p1.x() = 3;
  p2[1] = 4;
  std::cout << ((p1 == p2) ? "same point" : "different point") << std::endl;// => different point
  std::cout << ((0 == p1) ? "is zero" : "not zero") << std::endl;
  std::cout << ((p1 == 0) ? "is zero" : "not zero") << std::endl;
}

クラス外でしか定義できないoperatorというのは例えば二項演算子で左右の型が異なるとき、左が自身のオブジェクトでないときが挙げられます。上記の場合

C++
bool operator==(std::nullptr_t, const Point& r) noexcept { return r.x() == 0 && r.y() == 0; }
bool operator!=(std::nullptr_t, const Point& r) noexcept { return !(0 == r); }

ですね。

特殊メンバ関数とコンパイラによる暗黙宣言がある

さて、ここまでの説明でおかしなことがあります。次のようなCのコードを見てみましょう。

C99
typedef struct {
  float x;
  float y;
} Point;
int main(void)
{
  Point p1 = { 2, 3 };
  Point p2;
  p2 = p1;
  return 0;
}

C++は基本的にはCと互換があるので(完全ではない)このコードはC++のコードとしても有効なはずです。

ところが先に「オブジェクトをどのように生成するかを決めるのがコンストラクタ」と言いました。しかし上の例ではコンストラクタを定義していないのに

  Point p1 = { 2, 3 };

のようにオブジェクトを生成できています。さらに「後述する例外を除き勝手にオブジェクトに対する演算子を作ってはくれません」といいましたが、

  p2 = p1;

というコードの =は明らかにオブジェクトに対する演算子です。どういうことでしょうか?

特殊なメンバ関数

ずばり6種類です。

  • デフォルトコンストラクタ
  • コピーコンストラクタ
  • コピー代入演算子
  • ムーブコンストラクタ
  • ムーブ代入演算子
  • デストラクタ

実例を見てみましょう。std::stringに似せて作ったクラスです。ちょっと長いですが・・・。

C++
#include <cstring>
#include <cstddef>
#include <iostream>
#include <limits>
class inferior_string
{
private:
    char* m_s_;
    size_t m_len_;
    size_t m_capacity_;
public:
    //デフォルトコンストラクタ
    inferior_string() noexcept : m_s_(nullptr), m_len_(0), m_capacity_(0) {}
    //任意のコンストラクタ
    inferior_string(const char* str)
    {
        this->m_len_ = (nullptr == str) ? 0 : std::strlen(str);
        if(0 == this->m_len_){
            this->m_s_ = nullptr;
            this->m_capacity_ = 0;
        }
        else{
            this->m_capacity_ = ((std::numeric_limits<std::size_t>::max() / 2) < this->m_len_) ? std::numeric_limits<std::size_t>::max() : 2 * this->m_len_;
            this->m_s_ = new char[this->m_capacity_]();
            std::memcpy(this->m_s_, str, this->m_len_);//copy
        }
    }
    //コピーコンストラクタ
    inferior_string(const inferior_string& o) : m_len_(o.m_len_), m_capacity_(o.m_capacity_)
    {
        if(0 == o.m_len_){
            this->m_s_ = nullptr;
            this->m_capacity_ = 0;
        }
        else{
            this->m_s_ = new char[this->m_capacity_]();
            std::memcpy(this->m_s_, o.m_s_, o.m_len_);//copy
        }
    }
    //コピー代入演算子
    inferior_string& operator=(const inferior_string& o)
    {
        this->m_len_ = o.m_len_;
        if(0 == this->m_len_){
            delete[] this->m_s_;
            this->m_s_ = nullptr;
            this->m_capacity_ = 0;
        }
        else if(this->m_capacity_ < this->m_len_) {
            delete[] this->m_s_;
            this->m_capacity_ = ((std::numeric_limits<std::size_t>::max() / 2) < this->m_len_) ? std::numeric_limits<std::size_t>::max() : 2 * this->m_len_;
            this->m_s_ = new char[this->m_capacity_]();
            std::memcpy(this->m_s_, o.m_s_, o.m_len_);//copy
        } else {
            std::memcpy(this->m_s_, o.m_s_, o.m_len_);//copy
            this->m_s_[this->m_len_] = '\0';
        }
        return *this;
    }
    //ムーブコンストラクタ
    inferior_string(inferior_string&& o) noexcept
    : m_s_(o.m_s_), m_len_(o.m_len_), m_capacity_(o.m_capacity_)
    {
        o.m_s_ = nullptr;//disable input object's destructor.
        o.m_len_ = o.m_capacity_ = 0;
    }
    inferior_string& operator=(inferior_string&& o) noexcept
    {
        delete[] this->m_s_;
        this->m_s_ = o.m_s_;
        this->m_len_ = o.m_len_;
        this->m_capacity_ = o.m_capacity_;
        o.m_s_ = nullptr;//disable input object's destructor.
        o.m_len_ = o.m_capacity_ = 0;
        return *this;
    }
    ~inferior_string() noexcept
    {
        delete[] this->m_s_;
    }
    const char* c_str() const noexcept { return this->m_s_; }
    std::size_t length() const noexcept { return this->m_len_; }
    std::size_t size() const noexcept { return this->m_len_; }
    std::size_t capacity() const noexcept { return this->m_capacity_; }
    bool empty() const noexcept { return 0 == this->m_len_; }
};
std::ostream& operator<< (std::ostream& os, const inferior_string& str){
    if(!str.empty()) os << str.c_str();
    return os;
}
int main()
{
    inferior_string str = "arikitari";
    inferior_string str2 = str;//コンストラクタの呼び出し
    std::cout << str << '(' << static_cast<const void*>(str.c_str()) << "), " << str2 << '(' << static_cast<const void*>(str2.c_str()) << ')' << std::endl;
    inferior_string str3 = std::move(str);//ムーブコンストラクタの呼び出し
    //inferior_string str3 = static_cast<inferior_string&&>(str);//同じ意味
    std::cout << str3 << '(' << static_cast<const void*>(str3.c_str()) << ')' << std::endl;
    str = inferior_string("sekai");//ムーブ代入演算子の呼び出し
    std::cout << str << '(' << static_cast<const void*>(str.c_str()) << ')' << std::endl;
    str2 = str;//コピー代入演算子の呼び出し
    std::cout << str << '(' << static_cast<const void*>(str.c_str()) << "), " << str2 << '(' << static_cast<const void*>(str2.c_str()) << ')' << std::endl;
    return 0;
}
出力例
arikitari(0x1476c20), arikitari(0x1476c40)
arikitari(0x1476c20)
sekai(0x1477c70)
sekai(0x1477c70), sekai(0x1476c40)

デストラクターは生成されたオブジェクトの寿命が尽きたときに呼び出されます。上記例ではmain関数のreturn 0;のあとに実行されます。

まとめると、クラス名をXとした時、

種類 宣言
デフォルト・コンストラクタ X()
デストラクタ ~X()
コピー・コンストラクタ X(const X&)
コピー代入演算子 X& operator=(const X&)
ムーブ・コンストラクタ X(X&&)
ムーブ代入演算子 X& operator=(X&&)

のようになります。ただし、厳密にはもうちょっと制約が緩く、例えばコピーコンストラクタとムーブコンストラクタは

C++11: Syntax and Feature#クラスのコピーとムーブ
コピーコンストラクター(copy constructor)とは、あるクラスXにおいて、非テンプレートなコンストラクターで、一つ目の仮引数の型が、X &、const X &、volatile X &、const volatile X &のいずれかであり、二つ目以降の仮引数は存在しないか、すべてデフォルト実引数があるものをいう。

ムーブコンストラクター(move constructor)とは、あるクラスXにおいて、非テンプレートなコンストラクターで、一つ目の仮引数の型が、X &&、const X &&、volatile X &&、const volatile X &&のいずれかであり、二つ目以降の仮引数は存在しないか、すべてデフォルト実引数があるものをいう。

という定義になっています(がそこまで意識することってない)。

コンパイラによる暗黙宣言

でこの6つですが、暗黙に生成されることがあります。その条件は実に複雑で解説は

に譲りますが、いくつかの例を見てみましょう。なお「ユーザー」とはコードを書くあなた自身のことでコンパイラと対比して用いています。

  1. 何もコンストラクタ/代入演算子をユーザー宣言しなかった場合→すべて暗黙生成される
  2. 上記以外のコンストラクタをユーザー宣言した場合→デフォルトコンストラクタ以外は暗黙生成される
  3. デストラクタをユーザー宣言した場合→デフォルトコンストラクタは生成される。コピーコンストラクタ/代入演算子は生成されるが使用は非推奨。ムーブンストラクタ/代入演算子は生成されない
  4. デフォルトコンストラクタをユーザー宣言した場合→それ以外はすべて生成される

ということで結論は

  • デストラクタはユーザー宣言するな、=default指定(後述)も使うな、定義するならありったけ全部書け
  • C構造体はC++的には「何もコンストラクタ/代入演算子をユーザー宣言しなかった場合」に該当する

となります。

default/delete指定のすゝめ

コンパイラによる暗黙生成は規則がわかりにくいです。コードを書く上では後述するように結局覚えないといけませんが、読む人を考えるとdefault/delete指定をコンストラクタ/代入演算子につけたほうがいいと思われます。

C++
class ResourceManagerBase {
public:
    static constexpr std::size_t resource_limit_num = 99;
protected:
    int resources_[resource_limit_num];
private:
    int activeResource_;
public:
    ResourceManagerBase() noexcept;
    ResourceManagerBase(const ResourceManagerBase&) = delete;
    ResourceManagerBase(ResourceManagerBase&&) = delete;
    ResourceManagerBase& operator=(const ResourceManagerBase&) = delete;
    ResourceManagerBase& operator=(ResourceManagerBase&&) = delete;
    int activeResource() const noexcept;
    void activeResource(int handle) noexcept;
    void reset() noexcept;
    bool select(char c0, char c1) noexcept;
};

こんな感じですね。注意点として前節で「ユーザー宣言したとき」と言いましたが、default指定は「ユーザー宣言したとき」に含まれます。なので、デストラクタはdefault指定するべきではありません
また、

代入演算子のdefault指定とconstメンバ - yohhoyの日記

  • コンストラクタ/代入演算子などへdefault指定(=default)は、必ずしも“実装を定義する”ことを意味しない。
  • default指定は“暗黙的に行われるコンストラクタ/代入演算子の自動生成”をコンパイラに明示する。このルールに従った結果、コンストラクタ/代入演算子が定義されない(=delete指定相当)こともある。

とあるように、default指定しても必ずしも定義してくれるわけではありません。

メンバー初期化子とコンパイラ定義のデフォルトコンストラクタの挙動

コンストラクタを自分で書かない、もしくは= default指定したクラスをメンバーに持ったときに、メンバー初期化子でメンバー名()のようにメンバ変数を初期化した場合、そのクラス型のオブジェクトは0初期化されます。

一応根拠となる部分を江添さんの重箱本から引っ張っておきましょう。結論は上述の通りなので読み飛ばしていいです。

http://ezoeryou.github.io/cpp-book/C++11-Syntax-and-Feature.xhtml#dcl.init.default-initialize
8.5.3 値初期化(value-initialize)
値初期化(value-initialize)とは、T型のオブジェクトに対して、

Tが、unionではないクラス型で、ユーザー提供のコンストラクターを持たない場合、オブジェクトはゼロ初期化される。もし、暗黙的に定義されたコンストラクターが、トリビアルではない場合、コンストラクターが呼ばれる。

初期化子が空の括弧、()、であるとき、オブジェクトは値初期化される

後述の「強そうな人からclassに関連する謎な用語を使われたときにみる項」から飛べる記事で解説していますが、= default指定はユーザー宣言に含みますが、ユーザー提供ではないので、最初に「もしくは= default指定した」と書いたんですね。

C++
#include <iostream>
struct Vec2 {
  float x;
  float y;
};
class Weapon {
public:
  int  power;
  Vec2 velocity;
  Weapon() : power(), velocity() {}    
};
int main()
{
  Weapon obj;
  std::cout << obj.velocity.x << std::endl;// => 0
}

いや、当たり前なんですが、しかもC++11どころかC++03でも保証されている動作なんですが、
コンパイラによって動作が変わるコード @ゲームプログラマの小話[開発:コンパイラ・ビルド]
によると、そうならないコンパイラがあるそうで。・・・投げ捨てればいいと思うよ、そんなコンパイラ。

とりあえず少なくとも

必要のない努力
template<typename TPodType>
struct PodInitData
{
    /// テンプレートで指定した型をゼロ初期化したものを作成して返す。
    static TPodType Create()
    {
        TPodType tmp = {};
        return tmp;
    }
};
struct Vec2 {
  float x;
  float y;
};
class Weapon {
public:
  int  power;
  Vec2 velocity;
  Weapon() : power(0), velocity(PodInitData<Vec2>::Create()) {}    
};

こんな冗長なだけの無意味なコードは書く必要がないのは確かでしょう。いくらRVO効くだろうから実行コスト0でもコード量が無駄です(断じて @hoboaki さんをdisっているわけではないです、念のため)。

特殊メンバ関数の呼び出しのタイミング

ではここまで見てきた特殊メンバ関数はいつ呼び出されるのでしょうか?

何々の時に〇〇コンストラクタが呼出しされるが覚えられないので特訓

極めて秀逸な記事なので、そちらに丸投げします。

staticメンバ変数がある

通常のメンバ変数はオブジェクトごとに固有ですが、共有されるものとしてstaticメンバ変数があります。

といってもconst指定されたstaticメンバ変数かコンパイル時定数にしかほぼ使いません。
先程

C++
class ResourceManagerBase {
public:
    static constexpr std::size_t resource_limit_num = 99;

というコードを貼りましたが、これがstaticメンバ変数です。

staticメンバ関数がある

templateメタプログラミングでもしない限りお世話になることはありません。だってそれ以外はふつうのフリー関数(メンバ関数と対比してそう呼ぶ)でまにあっているので。

標準ライブラリ(STL)でこれを使っている例で有名なものとしては、limitsヘッダにあるstd::numeric_limitsでしょうか。実装はがっつりtemplateメタプログラミングしています。

継承できる

がっつりやりだすとvirtual云々という謎文章に襲撃されるのと実行速度が犠牲になるのでC++入門者な読者の皆様は距離を置くくらいでちょうどいいと思いますが(無理に使って悲惨なことになっているコードから目をそらしつつ)、全く知らないというのもどうかと思うので落とし穴が少そうなコードを紹介します。

C++
class Base {};
class Derived : public Base {};

継承は基本となる基底クラス(base class)があり、それを継承する派生クラス(derived class)があります(基底クラスは基本クラスとも言います)。Javaではスーパークラスとサブクラスと言うそうですが、C++では(C#もC++と同じ呼び方をするそうですね)そうは言いません。

派生クラスを作るには、派生クラス名のあとに :を書いてその後に基底クラス名を書きます。

継承3種類

  • public継承
  • protected継承
  • private継承

の3つがありますが、public継承以外に使うことはほぼありませんから忘れましょう。

C++
struct X : /* デフォルトでは public 継承 */ Base{

};

class X : /* デフォルトでは private 継承 */ Base{

};

派生クラスではコンストラクタとメンバ関数を追加するだけでメンバ変数は増やさないパターン

もう面倒になったので実装は書きません、クラス定義だけ。実装は

参照。

C++
class ResourceManagerBase {
public:
    static constexpr std::size_t resource_limit_num = 99;
protected:
    int resources_[resource_limit_num];
private:
    int activeResource_;
public:
    ResourceManagerBase() noexcept;
    ResourceManagerBase(const ResourceManagerBase&) = delete;
    ResourceManagerBase(ResourceManagerBase&&) = delete;
    ResourceManagerBase& operator=(const ResourceManagerBase&) = delete;
    ResourceManagerBase& operator=(ResourceManagerBase&&) = delete;
    int activeResource() const noexcept;
    void activeResource(int handle) noexcept;
    void reset() noexcept;
    bool select(char c0, char c1) noexcept;
};
class ImageResourceManager : public ResourceManagerBase {
public:
    ImageResourceManager() noexcept;
    ImageResourceManager(const ImageResourceManager&) = delete;
    ImageResourceManager(ImageResourceManager&&) = delete;
    ImageResourceManager& operator=(const ImageResourceManager&) = delete;
    ImageResourceManager& operator=(ImageResourceManager&&) = delete;
    bool load(const TCHAR* format) noexcept;
    int DrawGraph(int x, int y, bool transFlag) noexcept;
    int DrawRectGraph(int destX, int destY, int srcX, int srcY, int width, int height, bool transFlag, bool turnFlag = false) noexcept;
    int DerivationGraph(int srcX, int srcY, int width, int height) noexcept;
};

派生クラスでもメンバ変数を定義するとややこしさが倍増するので基底クラスでのみメンバ変数を定義するのはわかりやすさの観点で大事です(個人的な意見です)。

基底クラスでprivateと指定されているものは派生クラスから見えません。今回の場合はメンバ変数activeResource_が該当します。一方、基底クラスでprotectedと指定されているものはクラスの外からは見えないものの、派生クラスから見えます。今回はメンバ変数resources_ですね。

基底クラスでコピー/ムーブコンストラクタ/代入演算子がdelete指定されている時点で派生クラスもその影響を受けるのですが(これを利用したものとして`boost:nocopyableなんかがある)一応派生クラスでもdelete`指定しています。

派生クラスでメンバ変数を作らない&&派生クラス基底クラスともにデストラクタがないパターンはお手軽なので覚えておくといいと思います

 Empty Base Optimizationを期待しているパターン

C++11でもC++17でもRejectされたConceptという機能をどうにか現行のC++の範囲で実装することで可読性をあげようという試みはまあそこそこ見かけるんですが、その一例を見てみましょう(Conceptについては解説しません、templateが深く関係しているとだけ書いておきます)。

C++14
#include <cstdint>
#include <type_traits>
struct ConceptColorType {};
class RGB : public ConceptColorType {
public:
  std::uint8_t r;
  std::uint8_t g;
  std::uint8_t b;
};
class YCbCr : public ConceptColorType {
public:
  std::uint8_t y;
  std::uint8_t cb;
  std::uint8_t cr;
  YCbCr(RGB rgb);
  explicit operator RGB();
};
class YPbPr : public ConceptColorType {
public:
  std::uint8_t y;
  std::uint8_t cb;
  std::uint8_t cr;
  YCbCr(RGB rgb);
  explicit operator RGB();
};
template<
  typename ColorTypeFrom, typebame ColorTypeTo,
  std::enable_if_t<std::is_base_of<ConceptColorType, ColorTypeFrom>::value && std::is_base_of<ConceptColorType, ColorTypeTo>::value, std::nullptr_t> = nullptr
>
ColorTypeTo color_cast(ColorTypeFrom&& from){
  return {static_cast<RGB>(from)};
}

これは色を表すクラスですよーということを示すために中身が空っぽのクラスConceptColorTypeを継承させています。
あるクラスを継承しているかを調べるには<type_traits>ヘッダのstd::is_base_ofで簡単に判定できるので、こんな手法があります。

もっとも、decltypeハックで置き換えられることも多いように感じますが。

CRTP

Curiously Reccursive/Reccuring Template Patternです。上記のEBOを期待するパターンに近いのですがtemplateクラスを使います。もっというと基底クラスのtemplate引数が派生クラスの型を受け取るものです。

#include <utility>
template <class Derived>
class NonCopyable {
protected:
  NonCopyable() = default;
  NonCopyable(NonCopyable&&) = default;
  NonCopyable& operator=(NonCopyable&&) = default;
private: 
  NonCopyable(const NonCopyable&) = delete;
  NonCopyable& operator=(const NonCopyable &) = delete;
};
class Hoge : private NonCopyable<Hoge> {};
int main()
{
  Hoge hoge1;//OK
  Hoge hoge2;//OK
  //Hoge hoge3 = hoge1;//NG
  Hoge hoge4 = std::move(hoge1);//OK
  //hoge2 = hoge4;//NG
  hoge2 = std::move(hoge4);//OK
}

こんな感じで継承するだけでコピーできないクラスが作れます。

これを発展させると静的ポリモーフィズムなんてものができるっていうんだから怖い。
【C++】CRTPによる静的インターフェースクラスはライブラリ実装において不適切+解決法
静的ポリモーフィズムの安全で簡単な実装 -動的から静的にしてパフォーマンス向上-

virtual基底クラスを持てる

C++
class Base {};
class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};
class Derived3 : public Derived1, public Derived12 {};

解説しません。これが必要になる時点で継承関係が複雑すぎです。

virtual(仮想)関数を持てる

はい、普通じゃないメンバ関数です。殆どの場合、動的ポリモフィズムを実現するために継承とセットで用いられます。

基本的には普通のメンバ関数と同じですが、メンバ関数へのポインタをオブジェクトが持っていると言えばイメージが湧くんじゃないでしょうか。実際もっとも有名な仮想関数の実装方法であるvtableを使うものはまさにその通りになっています。

ただしポインタを介して、というくらいなので、通常の関数ポインタ同様最適化が難しく、inline展開などを阻害します。ですので、本当に速度が必要な場合は、templateや静的ポリモーフィズムやなにか根本的に別の手法で実現できないか試す必要が出てきます(そしてコードが魔界templateまみれになる)。

具体的な用例はC++入門者の範疇を超えるので割愛し、参考リンクを貼るにとどめます(というのは建前で私が仮想関数使うコードをあまり書かないので解説したくない)。

C#でおなじみ文脈依存キーワードのoverride/finalがC++にはあるんですね。override使おう。

templateの部分特殊化ができる

詳細は

に投げます。後述します。

自作したクラスをstd::unordered_mapでkeyとして使うためにstd::hashを特殊化するのが最も一般的な用法でしょうか。

関数オブジェクトとしてのクラス

主人公は operator()です。

そもそも関数は

C99
int hoge(void)
{
  return 3;
}
int main(void)
{
  int a = hoge();
  return 0;
}

関数名()で呼び出すのでした。つまり・・・

C++
#include <iostream>
struct F{
  int operator()()
  {
    return 3;
  }
};
int main()
{
  F f;
  auto r = f();// int型
  std::cout << r << ',' << F{}() << std::endl;// => 3,3
}

みたいに、operator ()をoverloadすれば関数もどきが作れるわけです。

operator ()がoverloadされているクラスを、関数オブジェクトないし、function-like classと呼びます。

関数ポインタよりコンパイラの最適化を阻害せず高速化できるため、よく用いられます。

C++にもlambda式と言うものがあるのですが、これはこの関数オブジェクトを自動生成するシンタックスシュガーだったりします。lambda式導入によって直に関数オブジェクトを作る機会は減りましたが、仕組みそのものは覚えておくといいかと思います。

詳細は
関数の創世から深淵まで駆け抜ける関数とはなんぞや講座
を見てください。

イテレータのためのクラス

C++には配列のようなものが何種類かありますが、これをまとめて「コンテナ」という概念であつかい、これを操作するために「イテレータ」という概念を導入しています。

c++
#include <array>
#include <iostream>
int main()
{
  std::array<int> arr = {{ 2, 3, 5 }};
  //イテレータを直に扱う
  for(auto it = std::begin(arr); it != std::end(arr); ++it) {
    std::cout << *it << std::endl;
  }
  //イテレータをRange-based forを介して扱う
  for(auto&& e : arr) {
    std::cout << e << std::endl;
  }
}

イテレータは自分で書くこともできます。
ただし書くのはだいぶダルいです。

いろんなイテレータを作っている例は
イテレータの解説をするなんて今更佳代
を御覧ください。

タグディスパッチのためのクラス

C++には例外がありますが、書いていると例外を使う版と使わない版の両方が欲しくなってくることがあります。

そこでタグディスパッチです。

C++標準ライブラリ(STL)には

STL
namespace std {
  struct nothrow_t {};
  extern const nothrow_t nothrow;
}

のような物があります。一体このstd::nothrow_tをどう使うのかというと

C++
class sound final {
//略
int play(playtype PlayType, std::nothrow_t) noexcept {
    return DxLib::PlaySoundMem(GetHandle(), static_cast<int>(PlayType), true);
}
void play(playtype PlayType, bool TopPositionFlag) {
    DXLE_SOUND_ERROR_THROW_WITH_MESSAGE_IF((-1 == this->play(PlayType, TopPositionFlag, std::nothrow)), "fail DxLib::PlaySoundMem().");
}
};

こんなふうに引数に使って関数のoverloadをします。すると

C++
int main(){
  sound s;
  //なんか
  s.play();//例外を投げる版を呼ぶ
  s.play(std::nothrow);//無例外版を呼ぶ
}

という呼び分けができるんですね。

STLにある他のタグディスパッチ用クラスとしては、std::piecewise_constructなんかがあります。

拡張メンバ関数を作るためのoperator overloadするためのクラス

散々ネタにされる話として
「C++ってstd::stringにsplitメンバ関数ないんでしょ?クスクス」
というお話があります。

よろしい、ならば戦争だ!

戦争に使う武器はoperator overloadです。operator overloadするにはクラス型を定義する必要がありました。

残念ながらoperator .は上述のとおりoverloadできないので、代わりにoperator |をoverloadします。

目指すは

C++
int main()
{
  std::string s = "arikitari na sekai";
  const auto re = s | split(' ');
}

真面目に実装している記事は
C++でPStade.Oven(pipeline)風味なstringのsplitを作ってみた
です。とてもじゃないですがここに書くには余白が足りない。

ともかくこれで
??「C++ってstd::stringにsplitメンバ関数ないんでしょ?クスクス」
C#「しかも拡張メソッドすらないんですって」
C++「operator overloadできるから十分だし」
と言い返せますね。

コンパイル時型操作のためのクラス

C++
template<typename Type1, typename Type2> struct is_same {
  static constexpr bool value = false;
};
template<typename Type> struct is_same<Type, Type> {
  static constexpr bool value = true;
};

static_assert(true == is_same<int, int>::value, "err");
static_assert(false == is_same<int, char>::value, "err");

int main() {}

テンプレートを使って色々書いているとtemplate引数で渡された型についていろいろ調べたくなります。その時先に書いたtemplate部分特殊化を使うためにこんなクラスを書くことがあります。

へ~そーなんだ~、で構いません。

強そうな人からclassに関連する謎な用語を使われたときにみる項

を書こうとしたら全く入門者向けの話ではなくなったので別の記事にしました。

アクセス指定があると何が嬉しいか

ぶっちゃけオブジェクト指向するためにある機能だと思います。それ以外の用途が私には浮かばない。

Cにおいてもファイルわけをして、外部公開しない変数にstaticをつけたと思います。

つまりアクセス指定があるとこれと同じで外部との結合が減るんですね。例えばメンバ変数をprivateにすればそれはクラスの外からはいじられないわけです(断じて全部のprivate変数にgetter/setterを書けという話ではない)

すると、メンテナンス可能性が向上します。publicだととどこからか変数が書き換えられる心配がありますが、private/protectedならそれがないんですね(ポインタとキャストゴリ押しで書き換えられるだろとか言わない)。

classstructの違い

上記でも解説しましたし、
【初心者 C++er Advent Calendar 2015 11日目】C++ における class と struct の違い - Secret Garden(Instrumental)(Qiita版)
でも解説されていますが、
まとめると、アクセス指定がデフォルトでpublicかprivateかの差のみです。

C++
//A1とA2は全く同じ意味
struct A1 {
  int a;//public
  static constexpr int n = 10;
};
struct A2 {
public:
  int a;
  static constexpr int n = 10;
};

static_assert(A1::n == A2::n, "err");//OK

//B1とB2は全く同じ意味
class B1 {
  int a;//private
  static constexpr int n = 10;
};
class B2 {
private:
  int a;
  static constexpr int n = 10;
};

//static_assert(B1::n == B2::n, "err");//NG
//error: 'constexpr const int B1::n' is private within this context
//error: 'constexpr const int B2::n' is private within this context

//C1とC2は全く同じ意味
struct C1 : A1 {};
struct C2 : public A1 {};

static_assert(C1::n == C2::n, "err");//OK

//D1とD2は全く同じ意味
class D1 : A1 {};
class D2 : private A1 {};

//static_assert(D1::n == D2::n, "err");//NG
//error: 'constexpr const int A1::n' is inaccessible within this context

int main(){}

classstructをどう使い分けるか

オブジェクト指向プログラミングのツールとしてクラスを使うにはclassキーワードを使うことが多いように思います。

一方でコンパイル時計算のためのクラスなど、メンバが全てpublicであることが望ましい場合などはstructキーワードを利用することが多いように思います。

またCの構造体と同じような使い方をする場合についてもstructキーワードを使うことが多いように思います。

まとめ

classについて様々な機能や用例を見てきました。classはかなりいろいろなことに使われていてすべて取り上げるのは不可能です。少なくともオブジェクト指向プログラミングのためだけの機能ではないことがご理解いただけたかと思います。

ぜひクラスを実際に自分が書くコードで使っていただければな、と思います。

License

CC BY 4.0

CC-BY icon.svg

95
107
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
95
107