• Class / Function / Type

      std::
    • Header file

      <>
    • Other / All

    最終更新日時(UTC):
    が更新

    履歴 編集

    範囲for文が範囲初期化子内で生じた一時オブジェクトを延命することを規定 [P2718R0]

    このページはC++23に採用された言語機能の変更を解説しています。

    のちのC++規格でさらに変更される場合があるため関連項目を参照してください。

    仕様

    範囲for文

    for ( init-statement(opt) for-range-declaration : for-range-initializer ) statement
    

    において、for-range-initializer の中で生じた一時オブジェクトは、for-range-initializer で初期化される参照と同じ寿命まで延命される。

    ただし、次の場合には適用されない。

    • 一時オブジェクトが関数の引数として生成された場合
    • 一時オブジェクトの(この規定が適用されない場合の)寿命が for-range-initializer 完全式の終わりではない場合

    以下の例では、getstr() が返す一時オブジェクトが範囲for文の末端まで延命されている。

    import std;
    
    std::vector<std::string> getstr() {
      return {"hello", "notUB"};
    }
    
    int main()
    {
      for(auto&& c : getstr()[1]) {
        std::println("{}", c);
      }
    }
    

    出力

    n
    o
    t
    U
    B
    

    この機能が必要になった背景・経緯

    範囲for文をより安全に使えるように改善するために仕様が変更された。

    従来の仕様では、範囲for文を使う際に気が付きにくいダングリング参照を発生させる可能性があった。

    #include <vector>
    #include <string>
    #include <iostream>
    
    std::vector<std::string> getstr() {
      return {"hello", "UB"};
    }
    
    int main()
    {
      for(auto&& c : getstr()[0]) {
        std::cout << c << std::endl;
      }
    }
    

    このコードでは getstr() が一時オブジェクトとして std::vector<std::string> を返し、範囲for文の範囲はその0番目の std::string の参照で初期化される。

    この範囲for文は次のように展開される。

    #include <vector>
    #include <string>
    #include <iostream>
    
    std::vector<std::string> getstr() {
      return {"hello", "UB"};
    }
    
    int main()
    {
      {
        auto&& r = getstr()[0];
        auto b = r.begin();
        auto e = r.end();
        for(; b != e; ++b) {
          auto&& c = *b;
          std::cout << c << std::endl;
        }
      }
    }
    

    ここで、r を初期化しているのは getstr() の返した一時オブジェクトではなく、その0番目の要素の参照である。そのため一時オブジェクト std::vector<std::string> はこの行の終わりに破棄され、r はダングリング参照となる。

    C++23では、getstr() の呼び出しが for-range-initializer の中にあるため、返った一時オブジェクトは参照 r と同じ寿命になる。したがってダングリング参照は発生せず、このコードは安全である。

    例外規定について

    "一時オブジェクトが関数の引数として生成された場合" とは、次のサンプルコードにおける f2(T t) の実引数 t として、呼び出される関数のスコープで生成されるような場合である。

    // P2718R0より引用
    using T = std::list<int>;
    const T& f1(const T& t) { return t; }
    const T& f2(T t)        { return t; }
    T g();
    
    void foo() {
      for (auto e : f1(g())) {}  // OK: g()の戻り値は延命される
      for (auto e : f2(g())) {}  // 未定義動作
    }
    

    このような t は呼び出される関数から戻ると破棄されるから、その参照を返すことは未定義動作である。 ここで未定義動作になることは範囲for文の危険性と無関係なので、寿命を延長するという解釈ができないようにこの例外規定が入った。

    議論:

    この例外規定の解釈は難解であり議論がある。

    • この t は "一時オブジェクトの寿命が for-range-initializer 完全式の終わりではない場合" にも該当すると考えられる
    • この t は、構文的に見ると for-range-initializer の中で生じたとは言えないという意見もある
    • "for-range-initializer の中" を実行時のことだと解釈すると、そこから呼び出された関数の中なども含むことになるが、それを排除する規定が "一時オブジェクトの寿命が for-range-initializer 完全式の終わりではない場合" ではないか
      • インライン展開されたときなどに効いてくるのかもしれない

    検討されたほかの選択肢

    一時オブジェクトの寿命について、範囲for文に限定しない汎用的な方法も検討されたが、最終的には範囲for文の例外規定となった。

    関連項目

    参照