【UNITY β機能】ScriptableRendererLoop 使ってみる
ScriptableRendererLoopとは
Unity 2017から搭載される予定の機能で現在はβバージョンである 今までブラックボックスだったレンダリングのループをある程度カスタマイズできる
カリング、Depthクリア、不透明描画、半透明描画・・・ 等の処理を変更できる サンプルによると HDRやGIや色々と高度な処理ができるようである
まずはお試し
UNITYβ版ダウンロード
この機能を試すにはβ版が必要だ
Unity - Beta から ダウンロードしよう。
過去バージョンを取得する事も可能だが私は Unity 2017.1.0b8 を使った。
ScriptableRendererLoopサンプル ダウンロード
https://github.com/Unity-Technologies/ScriptableRenderLoop/tree/unity-2017.1b5 ここにサンプルがある。 TagでUNITYのバージョンを選びダウンロードするが、エディタを2017.1.0b5にしても同じビルドエラーだった
ビルドしたらエラーが出るので 今回は無慈悲にエラーをコメントアウトした おそらく Experimental.PostProcess のファイルが欠乏しているだけとおもわれるが
HDRRenderPipeline.csの using UnityEngine.Experimental.PostProcessing; と、 m_PostProcessContext にまつわる部分を全部消した
とりあえずビルドできるようになった
ちらコード解析
BasicRenderPipeline.cs をみてみる
public static void Render(ScriptableRenderContext context, IEnumerable<Camera> cameras) { foreach (var camera in cameras) { // Culling CullingParameters cullingParams; if (!CullResults.GetCullingParameters(camera, out cullingParams)) continue; CullResults cull = CullResults.Cull(ref cullingParams, context); // Setup camera for rendering (sets render target, view/projection matrices and other // per-camera built-in shader variables). context.SetupCameraProperties(camera); // clear depth buffer var cmd = new CommandBuffer(); cmd.ClearRenderTarget(true, false, Color.black); context.ExecuteCommandBuffer(cmd); cmd.Release(); // Setup global lighting shader variables SetupLightShaderVariables(cull.visibleLights, context); // Draw opaque objects using BasicPass shader pass var settings = new DrawRendererSettings(cull, camera, new ShaderPassName("BasicPass")); settings.sorting.flags = SortFlags.CommonOpaque; settings.inputFilter.SetQueuesOpaque(); context.DrawRenderers(ref settings); // Draw skybox context.DrawSkybox(camera); // Draw transparent objects using BasicPass shader pass settings.sorting.flags = SortFlags.CommonTransparent; settings.inputFilter.SetQueuesTransparent(); context.DrawRenderers(ref settings); context.Submit(); } }
なるほど、ここがRenderLoopで、ここのソースを変更する事で独自のRenderLoopを作る事が出来るようだ 中身は CommandBufferを使って 描画命令をQueueに投げている
新規プロジェクトに組み込む方法
新規プロジェクトに ScriptableRendererLoopを組み込むのは少し面倒だったのでメモする
環境
Unity 2017.1.0b8 で試した。
スクリプトの追加
サンプルのBasicRenderPipeline.csをそのまま拝借する
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Experimental.Rendering; // Very basic scriptable rendering loop example: // - Use with BasicRenderPipelineShader.shader (the loop expects "BasicPass" pass type to exist) // - Supports up to 8 enabled lights in the scene (directional, point or spot) // - Does the same physically based BRDF as the Standard shader // - No shadows // - This loop also does not setup lightmaps, light probes, reflection probes or light cookies [ExecuteInEditMode] public class BasicRenderPipeline : RenderPipelineAsset { #if UNITY_EDITOR [UnityEditor.MenuItem("RenderPipeline/Create BasicRenderPipeline")] static void CreateBasicRenderPipeline() { var instance = ScriptableObject.CreateInstance<BasicRenderPipeline>(); UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/BasicRenderPipelineTutorial/BasicRenderPipeline.asset"); } #endif protected override IRenderPipeline InternalCreatePipeline() { return new BasicRenderPipelineInstance(); } } public class BasicRenderPipelineInstance : RenderPipeline { public override void Render(ScriptableRenderContext renderContext, Camera[] cameras) { base.Render(renderContext, cameras); BasicRendering.Render(renderContext, cameras); } } public static class BasicRendering { // Main entry point for our scriptable render loop public static void Render(ScriptableRenderContext context, IEnumerable<Camera> cameras) { foreach (var camera in cameras) { // Culling CullingParameters cullingParams; if (!CullResults.GetCullingParameters(camera, out cullingParams)) continue; CullResults cull = CullResults.Cull(ref cullingParams, context); // Setup camera for rendering (sets render target, view/projection matrices and other // per-camera built-in shader variables). context.SetupCameraProperties(camera); // clear depth buffer var cmd = new CommandBuffer(); cmd.ClearRenderTarget(true, false, Color.black); context.ExecuteCommandBuffer(cmd); cmd.Release(); // Setup global lighting shader variables SetupLightShaderVariables(cull.visibleLights, context); // Draw opaque objects using BasicPass shader pass var settings = new DrawRendererSettings(cull, camera, new ShaderPassName("BasicPass")); settings.sorting.flags = SortFlags.CommonOpaque; settings.inputFilter.SetQueuesOpaque(); context.DrawRenderers(ref settings); // Draw skybox context.DrawSkybox(camera); // Draw transparent objects using BasicPass shader pass settings.sorting.flags = SortFlags.CommonTransparent; settings.inputFilter.SetQueuesTransparent(); context.DrawRenderers(ref settings); context.Submit(); } } // Setup lighting variables for shader to use private static void SetupLightShaderVariables(VisibleLight[] lights, ScriptableRenderContext context) { // We only support up to 8 visible lights here. More complex approaches would // be doing some sort of per-object light setups, but here we go for simplest possible // approach. const int kMaxLights = 8; // Just take first 8 lights. Possible improvements: sort lights by intensity or distance // to the viewer, so that "most important" lights in the scene are picked, and not the 8 // that happened to be first. int lightCount = Mathf.Min(lights.Length, kMaxLights); // Prepare light data Vector4[] lightColors = new Vector4[kMaxLights]; Vector4[] lightPositions = new Vector4[kMaxLights]; Vector4[] lightSpotDirections = new Vector4[kMaxLights]; Vector4[] lightAtten = new Vector4[kMaxLights]; for (var i = 0; i < lightCount; ++i) { VisibleLight light = lights[i]; lightColors[i] = light.finalColor; if (light.lightType == LightType.Directional) { // light position for directional lights is: (-direction, 0) var dir = light.localToWorld.GetColumn(2); lightPositions[i] = new Vector4(-dir.x, -dir.y, -dir.z, 0); } else { // light position for point/spot lights is: (position, 1) var pos = light.localToWorld.GetColumn(3); lightPositions[i] = new Vector4(pos.x, pos.y, pos.z, 1); } // attenuation set in a way where distance attenuation can be computed: // float lengthSq = dot(toLight, toLight); // float atten = 1.0 / (1.0 + lengthSq * LightAtten[i].z); // and spot cone attenuation: // float rho = max (0, dot(normalize(toLight), SpotDirection[i].xyz)); // float spotAtt = (rho - LightAtten[i].x) * LightAtten[i].y; // spotAtt = saturate(spotAtt); // and the above works for all light types, i.e. spot light code works out // to correct math for point & directional lights as well. float rangeSq = light.range * light.range; float quadAtten = (light.lightType == LightType.Directional) ? 0.0f : 25.0f / rangeSq; // spot direction & attenuation if (light.lightType == LightType.Spot) { var dir = light.localToWorld.GetColumn(2); lightSpotDirections[i] = new Vector4(-dir.x, -dir.y, -dir.z, 0); float radAngle = Mathf.Deg2Rad * light.spotAngle; float cosTheta = Mathf.Cos(radAngle * 0.25f); float cosPhi = Mathf.Cos(radAngle * 0.5f); float cosDiff = cosTheta - cosPhi; lightAtten[i] = new Vector4(cosPhi, (cosDiff != 0.0f) ? 1.0f / cosDiff : 1.0f, quadAtten, rangeSq); } else { // non-spot light lightSpotDirections[i] = new Vector4(0, 0, 1, 0); lightAtten[i] = new Vector4(-1, 1, quadAtten, rangeSq); } } // ambient lighting spherical harmonics values const int kSHCoefficients = 7; Vector4[] shConstants = new Vector4[kSHCoefficients]; SphericalHarmonicsL2 ambientSH = RenderSettings.ambientProbe * RenderSettings.ambientIntensity; GetShaderConstantsFromNormalizedSH(ref ambientSH, shConstants); // setup global shader variables to contain all the data computed above CommandBuffer cmd = new CommandBuffer(); cmd.SetGlobalVectorArray("globalLightColor", lightColors); cmd.SetGlobalVectorArray("globalLightPos", lightPositions); cmd.SetGlobalVectorArray("globalLightSpotDir", lightSpotDirections); cmd.SetGlobalVectorArray("globalLightAtten", lightAtten); cmd.SetGlobalVector("globalLightCount", new Vector4(lightCount, 0, 0, 0)); cmd.SetGlobalVectorArray("globalSH", shConstants); context.ExecuteCommandBuffer(cmd); cmd.Dispose(); } // Prepare L2 spherical harmonics values for efficient evaluation in a shader private static void GetShaderConstantsFromNormalizedSH(ref SphericalHarmonicsL2 ambientProbe, Vector4[] outCoefficients) { for (int channelIdx = 0; channelIdx < 3; ++channelIdx) { // Constant + Linear // In the shader we multiply the normal is not swizzled, so it's normal.xyz. // Swizzle the coefficients to be in { x, y, z, DC } order. outCoefficients[channelIdx].x = ambientProbe[channelIdx, 3]; outCoefficients[channelIdx].y = ambientProbe[channelIdx, 1]; outCoefficients[channelIdx].z = ambientProbe[channelIdx, 2]; outCoefficients[channelIdx].w = ambientProbe[channelIdx, 0] - ambientProbe[channelIdx, 6]; // Quadratic polynomials outCoefficients[channelIdx + 3].x = ambientProbe[channelIdx, 4]; outCoefficients[channelIdx + 3].y = ambientProbe[channelIdx, 5]; outCoefficients[channelIdx + 3].z = ambientProbe[channelIdx, 6] * 3.0f; outCoefficients[channelIdx + 3].w = ambientProbe[channelIdx, 7]; } // Final quadratic polynomial outCoefficients[6].x = ambientProbe[0, 8]; outCoefficients[6].y = ambientProbe[1, 8]; outCoefficients[6].z = ambientProbe[2, 8]; outCoefficients[6].w = 1.0f; } }
RenderPipelineクラスも必要
using System; namespace UnityEngine.Experimental.Rendering { public abstract class RenderPipeline : IRenderPipeline { public virtual void Render(ScriptableRenderContext renderContext, Camera[] cameras) { if (disposed) throw new ObjectDisposedException(string.Format("{0} has been disposed. Do not call Render on disposed RenderLoops.", this)); } public bool disposed { get; private set; } public virtual void Dispose() { disposed = true; } } }
Assets/BasicRenderPipelineTutorial/BasicRenderPipeline.asset 等の名前とフォルダは矛盾が無いようにしよう
Editorメニューから CreateAssetを行うようになっている
Asset作成
Editorメニューより RenderPipeline->CreateBasicRenderPipeline (上記のコード変更してたら その名前)で Assetを作成する 成功すれば BasicRenderPipeline.asset が作成されている Scriptプロパティには 上記のスクリプト名が入っている 何か問題があれば、ファイル名やフォルダが間違えていると思われる
アタッチ
メニューより Edit->ProjectSettings->Graphic ScriptableRendererLoopSettings に先ほど作った BasicRenderPipeline アセットを指定 すると RenderPipelineをカスタマイズできる
多分 シェーダーとか、Pipelineを変えないとまともに描画できないと思うけど
右辺値参照~完全転送まで100%理解するぞ! part8 おまけ。
おまけ
可変引数テンプレートの Parameter Bagを展開しながら 全部出力する
#include<iostream> using namespace std; struct Concat{ template <class... T> void operator ()(T&&... t) { concat_(std::forward<T>(t)...); } private: void concat_() { } template <class Head, class... Tail> void concat_(Head&& head, Tail&&... tail) { cout << head; concat_(std::forward<Tail>(tail)...); } }; auto main() -> int { Concat con; con("test int:", 1 ); return 1; }
上記のように
template
今はcoutに出力しているが 当然 operator << を実装したくなる
#include<iostream> #include<sstream> using namespace std; struct Concat{ template <class... T> Concat& operator ()(T&&... t) { concat_(std::forward<T>(t)...); return *this; } friend ostream& operator << (ostream &os, Concat& obj); private: stringstream ss_; void concat_() { } template <class Head, class... Tail> void concat_(Head&& head, Tail&&... tail) { ss_ << head; concat_(std::forward<Tail>(tail)...); } }; inline ostream& operator << (ostream &os, Concat& obj){ os << obj.ss_.str(); obj.ss_.str(""); return os; } auto main() -> int { Concat con; cout << con("test int:", 1 ) << endl; return 1; }
operator() で thisを返し operator << にて streamへ。
最期に 再帰の部分をフリー関数へ(私の好み)
#include<iostream> #include<sstream> using namespace std; ostream& concat_( ostream &ss_) { return ss_; } template <class Head, class... Tail> ostream& concat_(ostream &ss_, Head&& head, Tail&&... tail) { ss_ << head; return concat_(ss_, std::forward<Tail>(tail)...); } struct Concat{ template <class... T> Concat& operator ()(T&&... t) { concat_(ss_, std::forward<T>(t)...); return *this; } friend ostream& operator << (ostream &os, Concat& obj); private: stringstream ss_; }; inline ostream& operator << (ostream &os, Concat& obj){ os << obj.ss_.str(); obj.ss_.str(""); return os; } auto main() -> int { Concat con; cout << con("test int:", 1 ) << endl; return 1; }
右辺値参照~完全転送まで100%理解するぞ! part7 可変引数テンプレート
前回の
template<class T> void hoge(T&& x){ std::forward<T>(x); }
universal referenceを使うことで、右辺値参照も左辺値参照もどちらも入ってくる
std::forwardを使うことで、右辺値参照の場合は ムーブコンストラクタ、左辺値参照の場合は コピーコンストラクタ が実行される
これでほぼ完ぺきだけど 完全転送と呼ぶには hoge(x,y,z...) と、可変引数に対応しなければいけません!
可変引数テンプレート
まずは可変引数テンプレートの説明
可変引数(printfのような va_list)のような書式で コンパイル時に展開される
#include<iostream> using namespace std; void bar( int i, string& s ){ cout << i << s << endl; }; template<class... T> void hoge(T... t){ bar(t...); } auto main() -> int { hoge(1, string(" test") ); // hoge(1); // hoge(int) は宣言されていないので エラー return 1; }
上記コードの template<class... T> で、0個以上の引数をもつものにヒットする bar(t...); で、tを展開したパラメーターを引数にもつ barを呼ぶ
barに必要なだけオーバーロードすればいい
あとはこれを universal reference と std::forwardを使い
#include<iostream> using namespace std; void bar( int i, string s ){ cout << i << s << endl; }; template<class... T> void hoge(T&&... t){ bar(std::forward<T>(t)...); } auto main() -> int { string s("s"); hoge(1, s ); hoge(1, " test" ); // hoge(1); // hoge(int) は宣言されていないので エラー return 1; }
これで、どんな引数でも完全転送が出来ました!
ま、ビットフィールド等 工夫しなければ完全転送できないパターンもあるようですが 一般的には これでOK
クラスのコンストラクタを可変長テンプレートにして ごにょごにょ
前回なやんだ続きではあります
関数テンプレートは暗黙的インスタンス化可能だが、クラステンプレートは不可能 ならば コンストラクタを可変長テンプレートすればいいじゃない
#include<iostream> #include<sstream> using namespace std; struct Hoge{ void stats_(){}; stringstream ss_; template <class Head, class... Tail> void stats_(Head&& head, Tail&&... tail){ ss_ << head; stats_(std::forward<Tail>(tail)...); } template <class... T> Hoge (T&&... t){ stats_(std::forward<T>(t)...); } friend ostream& operator << (ostream& os, const Hoge& hoge); }; ostream& operator << (ostream& os, const Hoge& hoge){ os << hoge.ss_.str(); return os; } int main() { cout << Hoge("aaa,", "bbb,", "ccc,") << endl; }
つまり 関数オブジェクトは
#include<iostream> #include<sstream> using namespace std; struct Hoge{ void stats_(){}; stringstream ss_; template <class Head, class... Tail> void stats_(Head&& head, Tail&&... tail){ // std::cout << head; ss_ << head; stats_(std::forward<Tail>(tail)...); } template <class... T> Hoge& operator() (T&&... t){ stats_(std::forward<T>(t)...); return *this; } friend ostream& operator << (ostream& os, const Hoge& hoge); }; ostream& operator << (ostream& os, const Hoge& hoge){ os << hoge.ss_.str(); return os; } int main() { cout << Hoge()("aaa,", "bbb,", "ccc,") << endl; }
こうやればできたんだなーー
関数オブジェクトでは 自分を返して汎用的にしてみたけど、何を返すのが正しいのかは わからない
関数テンプレートは暗黙的インスタンス化できる
で終わりなんだけど、少し悩んでやっぱ無理そうだったのでメモ 関数テンプレートで実装出来るのだが、使い勝手や最適化など考えて 関数オブジェクトにしたかったが 関数とクラスの テンプレートの違いで出来なかった
追記:やっぱできたぜ。メンバをテンプレートにすればOK
クラスのコンストラクタを可変長テンプレートにして ごにょごにょ - ムラサメ研究ブログ
テンプレート関数は暗黙的インスタンス化可能です!
template<T> void hoge(T t){}; hoge(12);
可能というかむしろ、暗黙的に使うよね。
もちろん あえて明示的にも書ける
template<T> void hoge(T t){}; hoge(12);
クラスは暗黙的インスタンス化できない
#include<iostream> template <class T> struct Stats{ void operator()(T t){} const; }; int main() { Stats<int>()( 12 ); // Stats()( 12 ); // 暗黙的インスタンス化できない }
右辺値参照~完全転送まで100%理解するぞ! part6 Universal Referenceと std::forward
やっと UniversalReferenceですよ!
その前に復習
右辺値参照を引数にする時は あまり気にせず下記のようにするのが正しいのでしたね
#include <iostream> using namespace std; class X{ public: // explicit付けられない! X(X& x){ cout << "copy" << endl; } X(X&& x){ cout << "move" << endl; } }; void hoge(X x){ cout << "hoge(X)" << endl; } int main() { X x; hoge(x); hoge(move(x)); }
呼ぶときに std::move() しなければコピーコンストラクタ、つければムーブコンストラクタが働く これは 暗黙的変換を利用し XとX&& を受け入れています
でも、コピーせず左辺値参照を渡したい事多いよね? コピーするだけ無駄だし それに STLのなかには explicit付きのものしか呼べない関数もあったりで 明示的に行いたい
そんな時は愚直に 左辺値参照と右辺値参照の関数を2つ(const参照いれて3個)実装すればよい
#include <iostream> using namespace std; class X{ public: // explicit付けられる。付けれるなら絶対付けた方がよい explicit X(X& x){ cout << "copy" << endl; } explicit X(const X& x){ cout << "const copy" << endl; } explicit X(X&& x){ cout << "move" << endl; } }; void hoge(X& x){ cout << "hoge(X&)" << endl; } void hoge(const X& x){ cout << "hoge(const X&)" << endl; } void hoge(X&& x){ cout << "hoge(X&&)" << endl; } int main() { const X x; hoge( x ); hoge( std::move(x) ); }
このようにすれば、当然 右辺値の時と左辺値の時で処理をわけられる
しかし、毎回同じような関数が3個。引数が増えればさらに倍増していく
それを防ぐのが Universal Referenceである
Universal Reference
#include <iostream> using namespace std; class X{ public: X(){ cout << "constuct" << endl; } explicit X(X& x){ cout << "copy" << endl; } explicit X(const X& x){ cout << "const copy" << endl; } explicit X(X&& x){ cout << "move" << endl; } X& operator =(X& x){ if(&x==this) return(*this); cout << "copy assignment" << endl; return(*this); } X& operator =(const X& x){ if(&x==this) return(*this); cout << "const copy asignment" << endl; return(*this); } X&operator =(const X&& x){ if(&x==this) return(*this); cout << "move asignment" << endl; return(*this); } ~X(){ cout << "destuct" << endl; } void foo(){}; }; class Hoge{ public: template<class T> void hoge(T&& x){ // x_ = x if lvalue // x_ = std::move(x) if rvale cout << "hoge(T&&)" << endl; } private: X x_; }; int main() { Hoge h; cout << "____" << endl; { X x; h.hoge( x ); } cout << "____" << endl; { const X x; h.hoge( x ); } cout << "____" << endl; { X x; h.hoge( std::move(x) ); } cout << "____" << endl; { h.hoge( X() ); } cout << "____" << endl; { h.hoge( std::move(X()) ); } cout << "____" << endl; } constuct ____ constuct hoge(T&%) destuct ____ constuct hoge(T&%) destuct ____ constuct hoge(T&%) destuct ____ constuct hoge(T&%) destuct ____ constuct hoge(T&%) destuct ____ destuct
template
そして 関数テンプレート void hoge(T&& x) に Tが X&の時とX&&の時を考える
ここは 前回の 参照の圧縮を使えば TがX&の時は hoge(X& x)
TがX&&の時は hoge(X&& x) となる
ところが問題はコメント部分である 引数 xが 左辺値(X&) の時は copy assignment(X& operator =(X&)) を行いたいが 右辺値(X&&)の時は move assignment(X& operator =(X&&)) を行いたい
そんな時に使えるのが std::forward
実際に使ってみる
#include <iostream> using namespace std; class X{ public: X(){ cout << "constuct" << endl; } explicit X(X& x){ cout << "copy" << endl; } explicit X(const X& x){ cout << "const copy" << endl; } explicit X(X&& x){ cout << "move" << endl; } X& operator =(X& x){ if(&x==this) return(*this); cout << "copy assignment" << endl; return(*this); } X& operator =(const X& x){ if(&x==this) return(*this); cout << "const copy asignment" << endl; return(*this); } X&operator =(const X&& x){ if(&x==this) return(*this); cout << "move asignment" << endl; return(*this); } ~X(){ cout << "destuct" << endl; } void foo(){}; }; class Hoge{ public: template<class T> void hoge(T&& x){ x_ = std::forward<T>(x); cout << "hoge(T&&)" << endl; } private: X x_; }; int main() { Hoge h; cout << "____" << endl; { X x; h.hoge( x ); } cout << "____" << endl; { const X x; h.hoge( x ); } cout << "____" << endl; { X x; h.hoge( std::move(x) ); } cout << "____" << endl; { h.hoge( X() ); } cout << "____" << endl; { h.hoge( std::move(X()) ); } cout << "____" << endl; } constuct ____ constuct copy assignment hoge(T&&) destuct ____ constuct const copy asignment hoge(T&&) destuct ____ constuct move asignment hoge(T&&) destuct ____ constuct move asignment hoge(T&&) destuct ____ constuct move asignment hoge(T&&) destuct ____ destuct
std::forwardを使えば、T&、const T&、T&& と3つの引数のオーバーロードを1関数で処理できる
とてもすばらしい。
右辺値参照~完全転送まで100%理解するぞ! part5 参照の圧縮(reference collapsing)
右辺値参照の山場の一つ Universal Referenceまであと一歩
参照の圧縮とは
UniversalReferenceそのものだが
参照は(const等のストレージクラス除き)2種類しかない
X& (左辺値参照)、X&&(右辺値参照)
X&&& 等はエラーですし X& && 等、参照を重ねる事は出来ない
でも、templateを使うと不運にも参照が重なる事がある
template<class T> void hoge(T& t){ }
例えばこのようなテンプレートに X&型の変数を適用したら T&は X& & になり、参照が重なってしまう これらの参照が重なった時はどうなるのだろうか?
コンパイルエラーにすると、テンプレートに参照を渡すものがほとんど動かなくなる C++では当然 参照が重なった場合に明確な法則がある
#include <iostream> using namespace std; class X{ public: X(){ } X(X& x){ cout << "copy" << endl; } X(X&& x){ cout << "move" << endl; } }; int main() { using Y=X&; using Z=X&&; X x; X( (Y&)x ); X( (Y&&)x ); X( (Z&)x ); X( (Z&&)x ); } ---- copy copy copy move
template 以外に typedef (using) でも参照を重ねる事が出来るので 試してみた
結果は X& & -> X&
X& && -> X&
X&& & -> X&
X&& && -> X&&
と、結論からいえば すべて右辺値参照の時のみ右辺値参照。それ以外は左辺値参照となる
このことを 参照の圧縮(reference collapsing) と呼ぶ