C++23からauto(x)
の形式のキャストが可能になります。
template<std::copy_constructible T> void f(T x) { T p = auto(x); // ok、C++23から }
これに関連する仕様等のメモです。
prvalue値へのキャスト
auto(x)
の形式のキャストはx
をその型をdecay
した型の値としてキャストするものです。
型のdecay
とはその型からCV/参照修飾を取り除き、配列/関数はそのポインタに変換するものです。配列/関数以外の場合、auto
によるキャストはx
をその型のprvalueへキャストします。
そして、auto(x)
のキャストは単なる型変換ではなく、キャスト結果のprvalueはx
の値をコピーしたものになります。したがって、その結果はx
とは異なるオブジェクトとして得られ、x
の状態を変更しません(コピーコンストラクタが変なことしない限り)。
template<std::copy_constructible T> void f(T x) { // auto(x)式の値カテゴリはprvalue static_assert(std::same_as<decltype(auto(x)), T>); // pとxは異なるオブジェクト(コピーされる) T p = auto(x); // auto(x)後もxの利用は安全 x.use(); // pに変更を加えてもxには波及しない p.mutate(); // auto(x)は同じオブジェクトに対して何度も行える T p2 = auto(x); }
auto f(int) -> std::size_t; int main() { int a[5]{}; // 配列型、関数型の場合は対応するポインタ型のprvalueへキャスト auto* p1 = auto(a); auto* p2 = auto(f); }
コピーを行うため、T
はcopy_constructible
である必要があります。move_constructible
でしかない場合はキャストは失敗しコンパイルエラーとなります。
struct no_copy { no_copy() = default; no_copy(const no_copy&) = delete; }; int main() { non_copy nc{}; non_copy cp = auto(nc); // ng }
また、auto(x)
だけでなくauto{x}
も有効であり、どちらも同じ効果になります。
template<std::copy_constructible T> void f(T x) { // どちらも同じprvalueへのキャスト T p1 = auto(x); T p2 = auto{x}; }
細かい仕様の話
auto(x)
の形式のキャストは明示的型変換(関数スタイルキャスト)の一種であり、型名の代わりにauto
を使用するものです。この場合に使用可能なのは丁度auto
のみで、decltype
やauto&
、const auto&
だとかは使用できません。
template<std::copy_constructible T> void f(T x) { // すべてコンパイルエラー decltype(x); decltype(auto)(x); auto&(x); auto&&(x); const auto&(x); }
auto(x)
の形式のキャストにおいてはauto
は他の所と同じくプレースホルダ型として扱われており、その型はx
の型が推論された後で置き換えられます。その推論においては通常のauto
の推論と同様に、単一のテンプレートパラメータをもちそのテンプレートパラメータ型の引数を1つだけ受ける関数テンプレートに対してx
を渡した時にそのテンプレートパラメータに推論される型が取得されます。
// これらのautoに推論される型は auto c = x; auto(x); auto f() { return x; } // このような関数テンプレートに対して template<typename T> void hf(T); // xをそのまま渡した時のTに推論される型として取得される hf(x);
この場合の推論時に行われるx
の型から修飾を取り除いたりポインタ型に変換したりといった調整を型に対するdecay
と呼び、結果の型はstd::decay_t<decltype((x))>
で得られる型と一致します。これは、配列型・関数型以外の場合は元の型に対してprvalueになります。
こうして取得された型をT
とすると、auto(x)
のプレースホルダauto
はこの型T
で置き換えられ、T(x)
として通常の関数スタイルキャストとして処理されます。この式はx
をコピーしてT
の新しいオブジェクトを構築する式となります。これはauto{x}
においても同様です。
template<std::copy_constructible T> void f(T x) { // この4つは実は同じ意味 auto(x); T(x); auto{x}; T{x}; }
auto
を用いたキャストにおいては、{}
の中の初期化式は1つだけでないとその型が推論できないため渡せる式は1つに限定され、()
の場合はカンマ区切りの式とみなされやはり渡せる式は1つだけになります。また、推論の仕様上取得される型はx
と修飾だけが異なる同じ型となるため、大きな型変換は起こりません。したがって、()
と{}
の違い(式の評価順序や縮小変換の禁止など)はここでは顕在化せず両方は真に同じ意味を持ちます。
文法上auto(x), auto{x}
のx
には任意の式を渡すことができ、意味論的にも制限がないため、auto
によるキャストは必ずしも変数名のみに対して作用するわけではありません。とは言え、何が渡されたとしてもやることは変わらず、与えられる式の結果をその型のprvalue値へキャストすることです。
auto f() -> std::string; auto g() -> const std::string&; int main() { std::vector vec = {1, 2, 3, 4}; auto v1 = auto(vec); // ok、コピー auto v2 = auto(std::move(vec)); // ok、ムーブ auto s1 = auto(f()); // ok、コピー省略 auto s2 = auto(g()); // ok、コピー }
auto(x)
のx
が右辺値の場合はコピーではなくムーブされて結果が生成されます。さらに、x
がprvalueの場合はコピー省略によってauto(x)
ではコピーもムーブも発生しません(受けている変数が非参照ならば、そこに直接構築される)。
利点や用途
前述のように、auto(x)
はx
の素の型T
に対してT(x)
と同じ意味になります。さらに、auto(x)
によるコピーはauto var = x;
のような変数宣言でも同じことを達成できます。
template<std::copy_constructible T> void f(T x) { // この4つは実は同じ意味 auto(x); T(x); auto{x}; T{x}; // 次の3つの宣言は同じことを行う auto v1 = auto(x); auto v2 = x; T v3 = x; }
とすると、auto(x)
のキャストは冗長で無価値なものにしか見えなくなります。
auto
キャストが有用なのは、上記のようにT
が素直に得られず、一時変数を作る必要がない場合においてです。
例えばコンテナをテンプレートで受け取って、その先頭要素と同じ値をコンテナから削除したい場合を考えます。
// front()が呼べるコンテナコンセプト template<typename C> concept container = std::ranges::forward_range<C> and requires(C& c) { {c.front()} -> std::same_as<std::ranges::range_reference_t<C>>; }; // コンテナから先頭要素と同じ要素を削除する void pop_front_alike(container auto& x) { // 先頭要素が削除された後、3番目の引数はダングリング参照となる std::erase(x.begin(), x.end(), x.front()); // 予め先頭要素をコピーしておいて、それを使う auto tmp = x.front(); std::erase(x.begin(), x.end(), tmp); // 1行で書こうとすると面倒・・・ using T = std::decay_t<decltype(x.front())>; std::erase(x.begin(), x.end(), T(x.front())); }
.front()
は要素への参照を返し、std::erase()
の第3引数は要素型のconst
参照を受け取ります。そのため、std::erase()
の第3引数にx.front()
を直接渡すと先頭要素が削除された後(つまり処理が開始されてすぐ)にその参照はダングリング参照となり、UBです。それを回避するためには、先頭要素を予めコピーしてからstd::erase()
に渡すことが必要となります。
ここでは、コピーしてる変数tmp
はその後使うことはないため一時変数は導入しない方が望ましく、要素型T
は直接的に見えていないため取得が面倒になります。
そこで、auto(x)
を使用すると、それらの懸念を解消しつつ同じことをよりシンプルに記述できます。
// コンテナから先頭要素と同じ要素を削除する void pop_front_alike(container auto& x) { // auto(x)を使う std::erase(x.begin(), x.end(), auto(x.front())); }
前述のように、auto(x.front())
はx.front()
の結果をdecayしてコピーした新しいオブジェクトのprvalueを返します。std::erase()
の実行によってコピー元音オブジェクトが削除されても、auto(x.front())
でコピーされたオブジェクトには何の影響もありません。なお、auto(x.front())
で渡したオブジェクトはstd::erase()
の呼び出しが終わるまで有効であり、この場合に生存期間の問題は発生しません。
また、T
の名前が5文字以上の場合(おそらく多くの場合はそうなるでしょう)なら文字数のアドバンテージを得ることができます。さらに言えば、目が慣れればT(x)
よりもauto(x)
の方が一貫性が高くその意図が明確になるでしょう。
class very_long_name_my_class { ... }; auto f(const auto&) { ... } int main() { very_long_name_my_class v{}; int n = 10; long double l = 1.0; // vをコピーしてfに渡したい場合 f(very_long_name_my_class(v)); f(int(n)); f(long double(l)); // ng f(auto(v)); f(auto(n)); f(auto(l)); // ok }
struct my_class { my_class(const my_class&) noexcept(...) { ... } my_class& operator=(my_class&&) noexcept { ... } my_class& operator=(const my_class& other) noexcept(std::is_nothrow_copy_constructible_v<my_class>) { if (this == &other) { return *this; } // コピーしてムーブ代入することで実装する auto copy = other; *this = std::move(copy); // あるいは *this = my_class(other); // auto(x) *this = auto(other); return *this; } }
decay-copyとの違い
auto(x)
の行うようなコピーは規格書中ではdecay-copy
という用語でよく知られており、対応する説明専用のライブラリ関数も用意されています。
// 実際の名前はdecay-copy template<class T> constexpr decay_t<T> decay_copy(T&& v) noexcept(is_nothrow_convertible_v<T, decay_t<T>>) { return std::forward<T>(v); }
template<std::copy_constructible T> void f(T x) { auto v1 = auto(x); auto v2 = decay_copy(x); // これではダメなの? }
auto(x)
のような構文を新たに導入せずとも、この関数を標準化すれば同じことは達成できるように思えます。そうしないのは、auto(x)
とdecay_copy(x)
では前者がキャスト式となり後者は関数呼び出し式となることから、その振る舞いに違いがあるためです。
まず1つ目の違いは、decay_copy(x)
はx
がprvalueである場合にその引数でprvalueが実体化されてしまいコピー省略を妨げる点です。auto(x)
の場合はこれ自体がprvalueの式であるためx
がprvalueである場合はコピー省略によって一切のコンストラクタ呼び出しを伴いません(というか何もしません)。
auto f() -> std::string; int main() { std::string s1 = auto(f()); // コピー省略によって、s1はf()のreturn文の式から直接構築される std::string s2 = decay_copy(f()); // s2はf()の戻り値からムーブコンストラクタによって構築される }
2つ目の違いは、クラス型のプライベートへのアクセスが可能なコンテキストでdecay_copy()
はそのコンテキストを引き継げない点です。
class A { int x; public: A(); auto run() { f(A(*this)); // ok f(auto(*this)); // ok f(decay_copy(*this)); // ng } protected: A(const A&); };
この場合のdecay_copy(*this)
で実際にA
のコピーコンストラクタが呼ばれるのは、decay_copy()
の定義内のreturn
文においてであり、decay_copy()
はA
のfriend
ではないためそこからはprotected
であるA
のコピーコンストラクタにアクセスできません。
auto(*this)
は単なる式であるため、コピーが発生するのはその直接のコンテキストであるA::run()
の定義内であり、そこからは問題なくA
のコピーコンストラクタにアクセスできます。
この例は直接的ですが、friend
を介すると別のクラスのコンテキストにおいても同様の違いが観測されます。
class S; class A { public: A() = default; private: A(const A&); friend S; }; class S { public: S() = default; void f(A& a) { auto ca1 = auto(a); // ok auto ca2 = decay_copy(a); // ng } };
規格書における置き換え
前述のように、規格書においては以前からauto(x)
とほぼ同等の意味合いでdecay-copy
という用語が使用され、また説明専用の関数が使用されていました。この機能のもう一つの目的としてはそれらを置き換えることで規格書の記述をシンプルにすることも含まれています。
ただし、単にdecay-copy
と書かれていてもその意図が微妙に異なっているらしく、auto(x)
とは異なりx
がprvalueの場合でもコピーを行うこと(prvalueを実体化させること)を意図した場合があるようです。そのため、機械的な置き換えではなく、その微妙な意図を汲み取った上でauto(x)
と同じ意味でdecay-copy
が使用されている場所についてのみ置き換えを行なっています。次の表はその大まかな分類と方針をまとめたものです
規格表現 | C++20の例 | C++23からの例 |
---|---|---|
特定の式を指定したdecay-copy |
decay-copy(begin(t)) |
auto(begin(t)) |
特定の式を指定しないdecay-copy |
decay-copy(E) |
変更なし |
decay-copy の評価結果についての説明 |
decay-copy の呼び出しはコンストラクタを呼び出したスレッドで評価される |
auto によって生成された値はコンストラクタを呼び出したスレッドで実体化される |
表中の規格文章は一例で、それぞれranges::begin
、std::thread
のコンストラクタにおけるものです。
正直こんなニュアンスわかるはずもありませんが、今後はauto(x)
が使用されている場所はprvalueの実体化を意味しないコピーであり、引き続きdecay-copy
が使用されている場所はそうではない、と思うことができます。
コンセプト定義における利用例
少し変わった使用法になりますが、この機能はコンセプトの戻り値型制約において役立つ場合があります。
例えば、コンセプトにおいて何か呼び出し可能な型をチェックする際に、その戻り値型を特定の1つの型に指定したい場合を考えます。
// Fは引数なしで呼び出し可能であり、bool型を返してほしい template<typename F> concept returning_bool = std::invocable<F> and requires(F& f) { {f()} -> std::convertible_to<bool>; // bool型を返すという意味になっていない };
この場合に要求したいことが、「f()
の戻り値型がbool
型そのものであってほしいが別に修飾(&
や&&
)はついていても構わない。ただしbool
以外の型は遠慮してほしい」だった場合、std::convertible_to<bool>
ではその要求を表現できていません。なぜなら、std::convertible_to<bool>
だとbool
に変換可能な型であればOKという意味になってしまっているからです。例えば、ポインタ型を返す引数なしの関数は全てこの制約をパスしますが、それは明らかに意図するところではないでしょう・・・
std::same_as
を使うとちょうどbool
型のみという制約になりますが、これだと今度は少し厳しくなります。
template<typename F> concept returning_bool = std::invocable<F> and requires(F& f) { {f()} -> std::same_as<bool>; // bool&などが弾かれる };
この戻り値型制約(および他のところでコンセプトの第一引数の自動補完が働く場所)においては、コンセプトの第一引数にはdecltype((expr))
が充てられます。例えば上記の場合、std::convertible_to<decltype((f())), bool>
という制約がチェックされますが、decltype((expr))
で取得していることによってexpr
の値カテゴリの情報が含まれることになります。
この場合にその修飾情報を無視した素の型をチェックするのは意外に難しく(decay_t
したりremove_cvref_t
したりする必要がある)、戻り値型制約の利用は諦めざるを得なくなります
template<typename F> concept returning_bool = std::invocable<F> and std::same_as<std::remove_cvref_t<std::invoke_result_t<F>>, bool>; // ようやくほぼ意図通りになる
これは目を凝らさないと何してるのかわからないものがあり、戻り値型制約と比べると可読性に劣り、書くのもの大変面倒です。
このような場合にauto(x)
を使用すると、型のdecay
(std::remove_cvref_t
)を式としてスマートに記述することができます。
template<typename F> concept returning_bool = std::invocable<F> and requires(F& f) { {auto(f())} -> std::same_as<bool>; // 修飾を無視して戻り値型がboolであること! };
もちろん、auto(x)
によって戻り値のコピーが入る場合があるので若干制約の意味は異なることになるのでその点に注意が必要ではありますが、invoke_result_t
をremove_cvref_t
してのような謎の呪文を書かなくても、同じことを戻り値型制約の構文の範囲内で表現可能になります。
このことは、上記f()
のような関数呼び出し式だけでなく任意の式においても有用となるでしょう。個人的には、本来の用途よりもこちらの用法の方がよく使いそうな気がしています。