Chapter 04

その他、Modern C++っぽい言語要素

dec9ue
dec9ue
2021.12.18に更新

ムーブセマンティクスとオブジェクト構築や引き回しの方法を説明したので、だいたいこれでModern C++としてはOKなのですが、「こういう言語機能を意識しているとナウいよ!(あえてモダンと言わない)」というシャレオツ言語機能について軽く説明していきます。

それぞれ、ざっと流し読んでもらえれば、という感じです。コードを書くときにふと思い出して使ったり、人のコードを読んでいるときに「あ、これ、xxxで出たやつだ!」となるように情報を羅列するだけです。

auto推論

ここまでにすでに多用していますが、 auto を書くことで型宣言を省略できます。 auto は基本的に「推論対象から参照を取り払った型」を示します。

ローカル変数には型指定をしたい場合を除いて auto を使うのが基本になります。

void foo()
{
    auto v = std::vector<int>{...};
    auto f = foo();
}

関数戻り値を auto にすることも可能です。ただし、省略することでシグニチャが読みづらくなることもあるので、使用の是非には議論があると思います。

auto foo1()
{
    return std::make_unique<FooType>();
}

メンバー変数やグローバルなコンテキストでの変数に auto を使うことは通常、ないと思います。

戻り値型の後置

auto の説明をしたので、ついでに戻り値型の後置も説明しておきます。auto キーワードで戻り値型を省略したあと、\-> を付与することで戻り値型を後置することができます。

auto foo2() -> std::unique_ptr<FooType>
{
    return std::make_unique<FooType>();
}

まぁ、これはテンプレートを書くときに使う機能なので、当面使うことはないかも知れないですね。

アプリケーションでのコーディングで使う可能性があるのは後述するラムダ式での戻り値型指定です。ラムダ式では戻り値の型指定は後置でしかできません。

auto foo3() = []() -> std::unique_ptr<FooType> {
    return std::make_unique<FooType>();
}

constexpr / nullptr

コンパイル時に決定する定数については constexpr を使うのが正しい作法になっています。コンパイル時に決定する定数に const を使うのはModern C++では実質的に誤りです。

constexpr double pi_div_2 = std::numbers::pi / 2.0;
// const double pi_div_2 = std::numbers::pi / 2.0; は適切でない

また、同じようなノリで NULL を使わずに nullptr を使用するのが正しい作法になりました。これもすでに多用していますね。nullptr は今やほとんど使用しない生ポインタに対しても使えるほか、 unique_ptrshared_ptr に対しても使えます。

char* raw_ptr = nullptr;
unique_ptr<int> u_ptr = nullptr;
shared_ptr<int> s_ptr = nullptr;

イテレータと範囲for

C++では以前からイテレータの概念があり、for ループはイテレータで記述するのが基本ですが、構文がメンドクサイのもあって int で回しちゃう例も多いですよね。

#include <iostream>
#include <vector>

struct Obj
{
    public:
    int value;
    Obj(int i)
    :value(i)
    {
    }
};

std::vector<Obj> v{Obj(1),Obj(2),Obj(3)};

int main()
{
    for(std::vector<Obj>::iterator it = v.begin();it!=v.end();it++)
    // めんどくさすぎるでしょ。。。
    {
        std::cout << it->value << std::endl;
    }
   
   return 0;
}

範囲forはループの記述を大幅に改善します。

int main()
{
    for(const auto& item : v)
    // ループ構文がメチャクチャ簡単!
    {
        std::cout << item.value << std::endl;
    }
}

お約束として、範囲forでイテレータを受ける型の記述はコンテナに含まれる値の型に対して、おおまかに次のようになります。

  • プリミティブ型 → const autoauto
  • クラスオブジェクト
    • const auto& → リードオンリーのとき
    • auto& → 対象のオブジェクトを書き換えるとき/コンテナ側からデータをムーブアウトするとき

まぁ、だいたい関数の引数と同じですね。データをコンテナからムーブしてしまっていいかどうかはコンテナや対象のデータの特性を確認する必要があります。

using

おっさんは using と言われると有名な575の句である using namespace std; を思い浮かべると思うのですが、Modern C++では using の主な用途は typedef に相当する機能で、型エイリアスなどと呼称します。

using str_gen_func = std::string (*)(int); // typedef std::string (*str_gen_func)(int); とほぼ同じ

その他、どうしても名前空間を外したいものをピンポイントで名指しします。

#include <iostream>
#include <chrono>

int main()
{
    // cout , endlだけは名前空間外したい
    using std::cout, std::endl;

    // ユーザ定義リテラルの名前空間外し:後述します
    using std::literals::chrono_literals::operator"" h;
    // ただしliteralsについては*で名前空間外しすることも多い
    // using std::literals::chrono_literals::*;

    // :
}

てか、基本的に名前空間を using したりしません。std とかは std::string みたいな感じで、名前空間つけたまま使っちゃいます。まどろっこしいパッケージ名に対しては名前空間をパカッとおっぴろげにするのではなく、短い名称に割り付けなおしたりします。

namespace fs = std::experimental::filesystem;

if(fs::create_directory("new_directory") != true){
    // ....
}

override / delete / default

class / struct のメンバ関数の構成に関するキーワード群です。

override

virtual 関数を使わざるを得ない場合、オーバーライドする側の class / struct 定義に override をつけておくと実装漏れをエラーで検出してくれます。

class MyBaseClass
{
    public :
    virtual void foo() = 0;
}
class MyClass : public MyBaseClass
{
    virtual void foo() : override;
}

// MyClass::foo() の実装がなければエラー

delete

過去のC++ではデフォルト実装を拒否して実装しないコンストラクタや演算子を private にしてお茶を濁したりしていましたが、Modern C++では delete キーワードを使って実装しないことを明示します。

class MyClass
{
    // コピー代入演算子のデフォルト実装を拒否するケース
    bool operator=(const MyClass&) = delete;
}

default

コンストラクタや代入演算子をデフォルト実装にすることを明示するキーワードです。

class MyClass
{
    // コピー代入演算子のデフォルト実装を明示的に行うケース
    bool operator=(const MyClass&) = default;
}

noexcept

noexcept を関数のシグニチャにつけると例外を発生しない旨をコンパイラに伝えられるのでより最適化されたコードを生成できます。また、noexcept のシグニチャがついた関数だけで noexcept 関数を構成すれば安心です。ただし、noexcept を指定したにもかかわらず例外を発生すると std::terminate() が即座に呼ばれるということなので計画的なご利用が必要です。

class NoExceptClass
{
    void foo() noexcept;
}

「オブジェクト指向言語」の隆盛とともに鳴り物入りで登場した例外機構ですが、時代を経てその管理の厄介さや性能面での問題が明確に認識されてきています。noexcept キーワードはそのような時流を受け、出るべくして出てきた言語機構であるとも言えそうです。

ラムダ式とキャプチャ

コールバックなどを要求する関数に、名前のついた関数を渡す代わりに匿名の関数であるラムダ式を直接渡せるのは本当に便利です。その場にロジックを書けることでコードの可読性を大幅に向上できることがあります。

std::for_each() 関数の引数として渡す例を示します。

std::vector<int> v;
std::for_each( v.begin(), v.end(), [](auto i){
    std::cout << i << ",";
});

ラムダ式は std::for_each() に代表されるイテレーション系の処理のほか、非同期処理、コールバックなどに多用されます。これらの機能では関数だけではなく、周辺環境(コンテキスト)の情報ごと渡したいことが多く、関数ポインタだけでは不満なことが多いのでした。

ラムダは呼び出し元のコンテキストを「捕まえる」ためにキャプチャという機能を持っています。ラムダを活用するにはこのキャプチャについて理解しておく必要があります。

参照キャプチャ

ラムダ式側から現在のコンテキストに対する参照を掴ませる指定です。最初の []& を指定しておくとラムダからコンテキストをいじるときに参照経由でアクセスします。

std::vector<int> v;
int count = 0;

std::for_each( v.begin(), v.end(), [&](auto i){
    std::cout << i << ",";
    count++; // countへの参照にアクセスしている
});

std::cout << "total count is :" << count << std::endl;

ただ、非同期処理やコールバックを設定する場合、ローカル変数への参照を掴ませるとか恐ろしくてできないですね。

コピーキャプチャ

ラムダ式側から現在のコンテキストのデータをコピーする指定です。最初の []= を指定しておくとラムダ式のインスタンス生成時点でのコンテキストをコピーします。

次の例では、同期処理の例となっていますが、コピーキャプチャは非同期処理などで現在のコンテキストからラムダ式にデータを受け渡したいときに便利です。

std::vector<int> v;
int offset = 3;

std::for_each( v.begin(), v.end(), [=](auto& i){
    i += offset; // offsetがラムダのコンテキストにコピーされている    
});

コピーキャプチャされた値には暗黙に const 修飾されるので基本的に書き込めません。あまり書き込む用事もないと思いますが書こうとして驚かないで。。。

初期化キャプチャ

キャプチャ時にムーブすることもできます。初期化キャプチャという機能を使います。これは非同期処理にオブジェクトを渡したいときに便利です。変数名を指定して受け渡しの処理を指定することができます。

int main()
{
    MovableObj1 m1;
    auto f = std::async( std::launch::async, [m1 = std::move(m1)](){
        MovableObj2 m2;
        // なにかMovableObj(m1)を使った処理
        return m2;
    });
    auto m2 = f.get(); // ちなみにm2の型はMovableObj2
}

初期化キャプチャを使えば参照、コピー、ムーブなど自由自在です。

std::vector<int> v;

MovableObj m;
CopiableObj c;
ReferencableObj r;

std::for_each( v.begin(), v.end(), [m=std::move(m),c=c,&r=r](auto i){
    m; // mはムーブされている
    c; // cはコピーされた
    r; // rは参照である
});

キャプチャについては初期化キャプチャを使えば他に何も考えなくていいじゃん、と思いますが、他にも this キャプチャなど細かい仕様があります。細かいことを説明すると長くなるので気になった方は cppreference などの参考文献でも見てください。

リテラル

コードを読んでいると、リテラル関係の記述もそこそこ多く感じられますので簡単にだけ触れます。

生文字列リテラル

C++のコードを読んでいると R から始まる文字列をよく見ます。これは生文字列リテラルというものです。ダブルクオートやバックスラッシュなどをそのまま文字列の中に入れられます。UTF-8文字列をそのまま埋め込みたいときは u8R になります。

std::string s1 = R"(this string contains "double quote".)";
std::u8string s_utf8 = u8R"(this string contains "double quote"(utf-8).)";

#細かいところは気になったらいろいろ調べてみてください。

ユーザー定義リテラル

ユーザー定義リテラルと呼ばれるものも時々使用されます。もっともよく使用されるのは時刻ライブラリですね。つまり、主に単位の概念として使用されるのを想定した機構です。

#include <chrono>

using std::literals::chrono_literals::operator"" h;

auto hours = 4.5h; // hoursはstd::chrono::hours相当の型

ユーザー定義リテラルは operator"" suf という形で定義されます。これで suf というサフィックスが使えるようになります。

メンバ関数の修飾

これは別に新しい機能というわけでもないのですが、メンバ関数に対して実質的な第一引数である *this に制約をかけることができます。constvolatile などの制約が可能でしたが、左辺値、右辺値の区別も行えるようになっています。

class X
{
    public:
    void foo_1 (int i)
    {}
    void foo_2 (int i) const
    {}
    void foo_3 (int i) & // 左辺値のときだけ c.f. static foo_3s(X& x,int i)
    {}
    void foo_4 (int i) && // 右辺値のときだけ c.f. static foo_3s(X&& x,int i)
    {}
};

X lv;
lv.foo_3(0); // OK
// lv.foo_4(0); // NG
// X().foo_3(0); // NG
X().foo_4(0); // OK