C++ コア ガイドライン:関数のパラメータと戻り値のセマンティック

今日は、C++ コア ガイドラインにおける関数のルールに関する論文を締めくくります。前回の投稿は、関数のパラメーターと戻り値の構文に関するものでした。約 15 のルールを含むこの投稿は、それらのセマンティクスに関するものです。

詳細に入る前に、パラメーターのセマンティック ルール、戻り値のセマンティック ルール、および関数のその他のルールの概要を説明します。

パラメータ受け渡しのセマンティック ルール:

  • F.22:00 を使用 または 16 単一のオブジェクトを指定する
  • F.23:20 を使用 「null」が有効な値ではないことを示す
  • F.24:32 を使用 または 45 ハーフオープンシーケンスを指定する
  • F.25:52 を使用 または 65 C スタイルの文字列を指定する
  • F.26:72 を使用 ポインターが必要な場所で所有権を譲渡する
  • F.27:83 を使用 所有権を共有する

戻り値のセマンティック ルール:

  • F.42:98 を返す 位置を示す (のみ)
  • F.43:(直接的または間接的に) ポインターまたはローカル オブジェクトへの参照を返さない
  • F.44:106 を返す コピーが望ましくなく、「オブジェクトを返さない」という選択肢がない場合
  • F.45:115 を返さない
  • F.46:124 138 の戻り型です
  • F.47:147 を返す 代入演算子から。

その他の機能ルール:

  • F.50:関数が機能しない場合はラムダを使用する (ローカル変数を取得するため、またはローカル関数を記述するため)
  • F.51:選択肢がある場合は、オーバーロードよりもデフォルトの引数を優先してください
  • F.52:アルゴリズムに渡されるものを含め、ローカルで使用されるラムダでの参照によるキャプチャを優先します
  • F.53:返される、ヒープに格納される、または別のスレッドに渡されるなど、非ローカルで使用されるラムダで参照によるキャプチャを避ける
  • F.54:151 をキャプチャした場合 、すべての変数を明示的にキャプチャします (デフォルトのキャプチャはありません)
  • F.55:166 を使用しないでください 引数

パラメータ受け渡しのセマンティック ルール:

このサブセクションをかなり短くすることができます。ほとんどのルールは、ガイドライン サポート ライブラリへの投稿で既に説明されています。興味のある方は、引用された投稿をお読みください。最初のルール F.22 について少しだけ言いたいと思います。

F.22:170 または 181 単一のオブジェクトを指定する

T* を使用して単一のオブジェクトを指定するとはどういう意味ですか?ルールはこの質問に答えます。ポインタは多くの目的に使用できます。

<オール>
  • この関数で削除してはならない単一のオブジェクト
  • この関数で削除する必要があるヒープに割り当てられたオブジェクト
  • ヌルツァイガー (nullptr)
  • C スタイルの文字列
  • C配列
  • 配列内の場所
  • このように多くの可能性があるため、ポインターは単一のオブジェクトに対してのみ使用する必要があります (1)。

    既に述べたように、関数パラメーターに関する残りの規則 F.23 から F.27 をスキップします。

    戻り値のセマンティック ルール:

    F.42:191 を返す 位置を示す (のみ)

    逆に言うと。所有権を譲渡するためにポインターを使用しないでください。これは誤用です。以下に例を示します:

    Node* find(Node* t, const string& s) // find s in a binary tree of Nodes
    {
     if (t == nullptr || t->name == s) return t;
     if ((auto p = find(t->left, s))) return p;
     if ((auto p = find(t->right, s))) return p;
     return nullptr;
    }
    

    ガイドラインは非常に明確です。呼び出し元のスコープにない関数から何かを返すべきではありません。次のルールはこの点を強調しています。

    F.43:決して (直接または間接的に) ローカル オブジェクトへのポインタまたは参照を返します

    このルールは非常に明白ですが、いくつかの間接的な指示がある場合、簡単に見つけられないことがあります。問題は、ローカル オブジェクトへのポインタを返す関数 f から始まります。

    int* f()
    {
     int fx = 9;
     return &fx; // BAD
    }
    
    void g(int* p) // looks innocent enough
    {
     int gx;
     cout << "*p == " << *p << '\n';
     *p = 999;
     cout << "gx == " << gx << '\n';
    }
    
    void h()
    {
     int* p = f();
     int z = *p; // read from abandoned stack frame (bad)
     g(p); // pass pointer to abandoned stack frame to function (bad)
    }
    

    F.44:203 を返す コピーが望ましくなく、「オブジェクトを返さない」という選択肢がない場合

    C++ 言語は、T&が常にオブジェクトを参照することを保証します。したがって、オブジェクトはオプションではないため、呼び出し元は nullptr をチェックしてはなりません。 F.43 は local への参照を返すべきではないと述べているため、このルールは以前のルール F.43 と矛盾していません。 オブジェクト。

    F.45:214 を返さない

    T&&を使用すると、破棄された一時オブジェクトへの参照を返すよう求めています。それは非常にまずいです (F.43)。

    f() 呼び出しがコピーを返す場合、一時への参照を取得します。

    template<class F>
    auto&& wrapper(F f)
    {
     ...
     return f();
    }
    

    これらの規則の唯一の例外は、移動セマンティックの std::move と完全な転送の std::forward です。

    F.46:226 231 の戻り型です

    標準 C++ では、main を 2 つの方法で宣言できます。 void は C++ ではないため、移植性が制限されます。

    int main(); // C++
    int main(int argc, char* argv[]); // C++
    void main(); // bad, not C++
    

    2 番目の形式は 241 と同等です

    メイン関数は 0 を返します。メイン関数に return ステートメントがない場合は暗黙的に。

    F.47:253 を返す 代入演算子から。

    コピー代入演算子は T&を返す必要があります。したがって、あなたの型は標準テンプレート ライブラリのコンテナーと矛盾しており、「int と同じようにする」という原則に従っています。

    T&による戻りと T による戻りに​​は微妙な違いがあります:

    <オール>
  • 265
  • 274
  • 2 番目のケースでは、A a =b =c; などの操作のチェーンです。コピー コンストラクタとデストラクタの 2 つの追加呼び出しが発生する可能性があります。

    その他の機能ルール:

    F.50:関数が機能しない場合はラムダを使用します (ローカル変数をキャプチャするため、またはローカル関数を記述するため)

    C++11 には、関数、関数オブジェクト、ラムダ関数などの callable があります。関数またはラムダ関数をいつ使用する必要があるかという質問がよくあります。ここに 2 つの簡単なルールがあります

    • callable がローカル変数をキャプチャする必要がある場合、またはローカル スコープで宣言されている場合は、ラムダ関数を使用する必要があります。
    • callable がオーバーロードをサポートする必要がある場合は、関数を使用してください。

    F.51:選択肢がある場合は、オーバーロードよりもデフォルトの引数を優先する

    異なる数の引数で関数を呼び出す必要がある場合は、オーバーロードよりもデフォルトの引数を優先してください。したがって、DRY の原則に従います (同じことを繰り返さないでください)。

    void print(const string& s, format f = {});
    

    void print(const string& s); // use default format
    void print(const string& s, format f);
    

    F.52:キャプチャを優先するアルゴリズムに渡されるものを含め、ローカルで使用されるラムダの参照による

    パフォーマンスと正確さの理由から、ほとんどの場合、参照によって変数をキャプチャする必要があります。ルール F.16 によると、変数 p が sizeof(p)> 4 * sizeof(int) を保持する場合、効率が向上します。

    ラムダ関数をローカルで使用するため、キャプチャされた変数メッセージの存続期間の問題は発生しません。

    std::for_each(begin(sockets), end(sockets), [&message](auto& socket)
    {
     socket.send(message);
    });
    

    F.53:返される、ヒープに格納される、または別のスレッドに渡されるなど、非ローカルで使用されるラムダでの参照によるキャプチャを避ける

    スレッドを切り離す場合は、十分に注意する必要があります。次のコード スニペットには 2 つの競合状態があります。

    std::string s{"undefined behaviour"};
    std::thread t([&]{std::cout << s << std::endl;});
    t.detach();
    

    <オール>
  • スレッド t は、その作成者の寿命よりも長く存続する場合があります。したがって、std::string はもう存在しません。
  • スレッド t は、メイン スレッドの寿命よりも長く存続する可能性があります。したがって、std::cout はもう存在しません。
  • F.54:289 、すべての変数を明示的にキャプチャします (デフォルトのキャプチャなし)

    [=] によるデフォルトのキャプチャを使用しているように見える場合、実際にはすべてのデータ メンバーを参照によってキャプチャしています。

    class My_class {
     int x = 0;
    
     void f() {
     auto lambda = [=]{ std::cout << x; }; // bad 
     x = 42;
     lambda(); // 42
     x = 43;
     lambda(); // 43
     }
    };
    

    ラムダ関数は参照によって x をキャプチャします。

    F.55:292 を使用しないでください 引数

    関数に任意の数の引数を渡したい場合は、可変個引数テンプレートを使用します。 va_args とは対照的に、コンパイラは正しい型を自動的に推測します。 C++17 では、引数に演算子を自動的に適用できます。

    template<class ...Args>
    auto sum(Args... args) { // GOOD, and much more flexible
     return (... + args); // note: C++17 "fold expression"
    }
    
    sum(3, 2); // ok: 5
    sum(3.14159, 2.71828); // ok: ~5.85987
    

    奇妙に思われる場合は、fold 式に関する以前の投稿をお読みください。

    次は?

    クラスはユーザー定義型です。状態と操作をカプセル化できます。クラス階層のおかげで、型を整理できます。次の投稿は、クラスとクラス階層のルールについてです。