🤖

C++ モダンな書き方

2022/04/03に公開

自分用の殴り書き

はじめに

参考サイト

ムーブセマンティクス & 関数の引数と戻り値のベストプラクティス

C++では昔から左辺値と右辺値がありました。しかし、右辺値に対してはうまく扱う方法がありませんでした。

#include <vector>

void func() {

    // 右辺値 下のvectorのような式を抜けるとデストラクタが呼ばれるオブジェクト
    int i = (std::vector<int>{1,2,3}).pop();

    // 左辺値 式を抜けてもデストラクタが呼ばれないオブジェクト
    {
        std::vector<int> v;
        v.push_back(1);
        v.push_back(2);
    }
}

左辺値は変数vに束縛できていますが、右辺値を束縛する方法はC++03の場合はありませんでした。この右辺値はコピーされることでしか束縛できず、戻り値には用いることは効率性の観点から厳禁とされてきました。

#include <vector>

std::vector<int> func() {    
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    
    return v;
}

int main() {
    // コピーが動いてしまう
    std::vector<int> ret = func();

    // かといって参照もできない
    std::vector<int>& ret = func();    

    // コピーが動かないように、こういうことをしていた
    std::vector<int> ret;
    func2(ret);

    return 0;
}

そこでmoveが登場しました。moveには使う側の理屈と使われる側で決まった書き方のルールがあります。

// 昔のcppでは見なかったものが追加されています。★印部分。
template <typename T>
class stack
{
 public:
    stack(); // デフォルトコンストラクタ
    stack(const stack& rhs); // コピーコンストラクタ
    stack(stack&& rhs); // ★ムーブコンストラクタ 

    stack& operator=(const stack& rhs); // 代入演算子
    stack& operator=(stack&& rhs); // ★ムーブ代入演算子

 private:
    array* p_;
};

// ムーブと聞いてもイマイチピンと来ないので何をやっているか実装例を見せます
template <typename T>
stack<T>::stack(const stack& rhs)  // コピーコンストラクタ
    : p_(new array()) {
    
    for (auto e : rhs.p_) {
        p_->add(e);// コピーしています。コピー元もそのまま使えます
    }
}

template <typename T>
stack<T>::stack(stack&& rhs)  // コピーコンストラクタ
    : p_(nullptr) {
    
    p_ = rhs.p_;// コピーしていません。コピー元もそのまま使えます
    rhs.p_ = nullptr;// モーブ元は使えません
}

なんでムーブを示すメソッドには&&を付けるかというと関数オーバーロードの原則に従うためです。
つまり、同じ型引数を持つメソッドを複数同じクラスに持つことはできず、呼び出し側もどちらを呼ぶかを意識する必要があります。
そして、ムーブ元のオブジェクトは使えなくなることが多いため不必要なケースでムーブが動くようなことは内容になっています。

auto dst = std::move(src)); // std::moveを使うことでムーブを呼べます
auto dst = src; // 普通にコピーする限りはムーブは絶対に呼べません
auto dst = return_src(); // 戻り値に関しては特例としてmoveが呼ばれます。元のオブジェクトが不要となるため

RVO と NRVO

RVO(Return Value Optimization)とNRVO(Named Return Value Optimization)というコピーもムーブも動かないパターンがあります。
RVOとNRVOも一部のコンパイラが対応しており、C++17からはRVOに対して必須対応となっています。

Obj func_RVO() {
    // return式中で確保されるメモリは呼び出し側になるように最適化される。
    // つまり、ムーブもコピーも動かない
    return Obj{1,2,3};
}

Obj func_NRVO() {    
    // return式中で確保していなくても呼び出し側で確保できれば最適化される
    auto m = Obj{1,2,3};
    return m;
}

int main() {
    
    auto ret = func_RVO();  //func_RVO中でretに直接値が書き込まれる
    auto ret = func_NRVO(); //func_NRVO中でretに直接値が書き込まれる

    return 0;
}

戻り値と引数の選択


F.16: "in" パラメータでは、安価にコピーできる型は値で渡し、それ以外は const 型への参照で渡す。

void f1(const string& s);  // OK: pass by reference to const; always cheap

void f2(string s);         // bad: potentially expensive

void f3(int x);            // OK: Unbeatable

void f4(const int& x);     // bad: overhead on access in f4()

F.17: "in-out "パラメータでは,non-constの参照渡し

void update(Record& r);  // assume that update writes to r

F.18: "move"したいパラメータは、X&&とstd::moveで渡す。
F.19: "forward(転送)"したパラメータはTP&&で渡し、std::forwardのみとする。

template<class F, class... Args>
inline auto invoke(F f, Args&&... args)
{
    return f(forward<Args>(args)...);
}

ちょっと説明すると、引数で&&で受けたものは関数中へ渡されるときにムーブが動きますがそれ以降は左辺値として扱われます。
このためfが右辺値を受け取る関数だとコンパイルエラーになってしまいます。
ならば、

template<class F, class... Args>
inline auto invoke(F f, Args&&... args)
{
    return f(std::move(args)...);
}

とすると、ムーブが動くのですが、今度はfが左辺値を引数に期待しているとコンパイルエラーとなります。
なので、

  • 参照元が右辺値なら、引数を右辺値に
  • 参照元が左辺値なら、引数を左辺値に

というテンプレートメタプログラミングが必要になります。それを初めからやってくれるのがstd::forwardです。

F.20: "out "出力値については、return valuが望ましい。

F.21: 複数の "out "値を返すには、構造体かタプルを返すのが好ましい

範囲for

for(std::vector<Obj>::iterator it = v.begin();it!=v.end();it++) {
    std::cout << it->value << std::endl;
}

for(const auto& item : v) {
    std::cout << item.value << std::endl;
}

for文中の型の選択

  • プリミティブ型
    const autoauto
  • クラスオブジェクト
    • const auto&
      リードオンリーのとき
    • auto&
      対象のオブジェクトを書き換えるとき
  • テンプレートなど汎用的な場面
    ユニバーサル参照auto&&を使ってください。ユニバーサル参照は左辺値参照と右辺値参照のいずれか適切な型を取ります。

using

可読性を上げるための型エイリアス目的では積極的に使いましょう。

using IO = FOO::BAR::HOGE::IO;

if (IO::FILE = type) return 0;
if (IO::SOCKET = type) return 1;
if (IO::STDIN = type) return 2;
if (IO::STDOUT = type) return 3;

override

派生クラスでオーバーライドしているメソッドにはoverride指定子を付けて下さい。

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

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

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

昔はオーバーライドしていることを意思表示するためにMyClass::fooにvirtualを付けていましたがもう不要です。

delete

過去のC++ではデフォルト実装があると困る場合、例えばコピー不可の場合はコピーコンストラクタをprivate宣言したりしています。今後はこうします。

class MyClass
{
    MyClass(const MyClass&) = delete;
    MyClass(MyClass&&) = delete;
    MyClass& operator=(const MyClass&) = delete;
    MyClass& operator=(MyClass&&) = delete;
}

default

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

ラムダ式

ラムダ式を適切に使用することで開発効率や可読性の向上が期待できる場合は使用してください。
ただ、その際に以下の点にはご注意ください。

  • ラムダ式をローカルで使用する
    変数のキャプチャは参照か値のいずれでも構いません。処理効率を考えると参照がいいと考えます。
// 本当はconstなローカル変数にしたいが特定の初期化手順が必要な場合に
// ラムダを使用したカプセル可が期待できます。
const widget x = [&] {
    widget val;  // assume that widget has a default constructor
    for (auto i = 2; i <= N; ++i) {            // this could be some
        val += some_obj.do_something_with(i);  // arbitrarily long code
    }                                          // needed to initialize x
    return val;
}();
  • ラムダ式を別スレッドや所有権を手放す
    変数のキャプチャは値にしてください。
int local = 42;
// Want a copy of local.
// Since a copy of local is made, it will
// always be available for the call.
thread_pool.queue_work([=] { process(local); });

生文字リテラル

エスケープシーケンスなしで色々設定文字列を指定できます。便利なので使いましょう。
utf-8版もあります。

std::string s  =  "(this string contains \"double quote\".)";// \が必要になる
std::string s1 = R"(this string contains "double quote".)";
std::u8string s_utf8 = u8R"(this string contains "double quote"(utf-8).)";

文字列

基本的にはstd::stringを使って下さい。引数で受け取る時はstd::string_view使うほうがいいかもしれません。

Discussion