C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

C#: null れる class と null れない struct いずれの可能性もある object を as<T> したい場合の限界、あるいは C# らしい結論

// これはコンパイルできない(´・ω・`)
// object -> T
static public T As<T>( object o )
{ return o as T; }
// as T できるためには T に class 制約が必要。これはコンパイルできるし、期待動作するかもしれない。
// object(class=nullれる何か) -> T
static public T As<T>( object o )
  where T: class
{ return o as T; }
// これは As<T> が期待動作する。
object a = "Я(やー)";
var aa = As<string>( a );
Console.Write( aa );
// これは As<T> をコンパイルできないから期待動作できない。
object b = 4649;
var bb = As<int>( b ); // <-- oops!
Console.Write( bb );
// これはコンパイルできない(´・ω・`)
static public T? AsNullable<T>( object o )
{ return o as T?; }
// as T? できるためには T に struct 制約が必要。これはコンパイルできるし、期待動作するかもしれない。
static public T? AsNullable<T>( object o )
  where T: struct
{ return o as T?; }
object b = 4649;
var bb = AsNullable<int>( b ); // int? 型になる。
// 暗黙的に int 型のフリが得意な struct をラップした class 的に振る舞う。
Console.WriteLine( $"bb={bb} (bb==null)={bb==null}" );
// null れる
bb = null;
Console.WriteLine( $"bb={bb} (bb==null)={bb==null}" );
// でも(´・ω・`)…
int bbb = bb; // <-- oops! int? は int になれないのでコンパイルエラー。
// 「int? -> int または無かった時の int な補助要員を用意」ならできる
int bbb = bb ?? -1;
// つまり…(´・ω・`) これなら object から int とか struct な T へも As<T> できる。一応。
static public T As<T>( object o, T default_value )
  where T : struct
{ return o as T? ?? default_value; }
// 一応、期待動作する。
object b = 4649;
int bbb = As( b, -1 ); // 型パラメーターは第2実引数から推論できるので省略させられる
// つまり… 2つオーバーロードしておいて…
// 対 class 用 As<T>
static public T As<T>( object o ) where T : class { return o as T; }
// 対 struct 用 As<T>
static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// int, float, double, string; string は class, そのほかは struct
var os = new object[]{ 123, 1.23, 12.3f, "hoge" };
// 一応、 どちらにも As<T> できないこともない…(´・ω・`)
foreach ( var o in os )
  Console.WriteLine( $"{As( o, -1 )}, {As( o, -2.0 )}, {As( o, -3.0f )}, {As<string>( o )}" );
// こんなんが出る。期待動作といえる場合もあるけど、言えない場合もある(´・ω・`)
// 型判定が厳密過ぎて As というより Is Then って感じだね(´・ω・`)
123, -2, -3, 
-1, 1.23, -3, 
-1, -2, 12.3, 
-1, -2, -3, hoge

で、この As<T>As<T> を使う実装者が object の中身が classstruct か知っていないと使えない。そういうわけで「一応」が抜けない(´・ω・`)

// これはダメ、コンパイルエラー。C#のメソッドのシグニチャーに where 句は含まれないので同じシグニチャーのメソッドの重複定義。
static public T As<T>( object o, T default_value ) where T : class { return o as T ?? default_value; }
static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// これは期待動作しちゃう。シグニチャーは異なる事になってコンパイルできるし、さっきの os と As<T> の使い方なら期待動作する。
static public T As<T>( object o, object default_value = null ) where T : class { return o as T ?? default_value as T; }
static public T As<T>( object o, T default_value ) where T : struct { return o as T? ?? default_value; }
// でも、こうすると死ぬので「期待動作しちゃう」だった。
var os = new object[]{ 123, 1.23, 12.3f, "hoge" };
foreach ( var o in os )
  Console.WriteLine( $"{As( o, -1 )}, {As( o, -2.0 )}, {As( o, -3.0f )}, {As( o, "fuga" )}" ); // <-- 最後の As も書き方を統一してみちゃうと死ぬ。

As<T>( o, "fuga" )class 制約の方が呼ばれて欲しいんだけど、そんなプログラマーのお気持ちは言語仕様は組んでゆるふわ動作してくれないから、死ぬ。

// あーれこれと考えた結果、たどり着いた C# の限界に無理をしない As<T> たち
static public T AsClass<T>( object o ) where T : class { return o as T; }
static public T? AsStruct<T>( object o ) where T : struct { return o as T?; }
// ついでにおまけ、(double)34.56 を (int)35 に As りたいとかそーゆー場合用
static public T AsConvertible<T>( object o ) where T : IConvertible { return (T)Convert.ChangeType( o, typeof(T) ); }

まあ…、一応考えてはみましたよ、内部的には AsClass<T>AsStruct<T> も試した結果を (TC,TS?) なタプルで返して…とか、 class X<TC,TS> な感じで classobjectstruct?object をプロパティーで保持しつつ、 operator 群, Equals, GetHashCode, GetType をよしなに実装した薄いラッパーを As<T> で返させて…とか、いっそ実引数で型パラメーター相当の…とか。

でも、 C# の言語仕様の限界をそんな疲れて保守性も悪そうな何かを必死に作ってどうにかせんでも…と思い、そもそもそうなると↑の AsClass, AsStruct, AsConvertible も何かの部品に internal で使うわけでなければ、ほとんど存在価値がないどころか無駄なわけで…。

// 実行時に謎の object をあれかこれかそれか…って処理したいなら(´・ω・`)
void f( object o )
{
  switch ( o )
  {
    case IConvertible convertible: // ... 略
    case MyStructX sx: // ... 略
    case MyClass1 c1: // ... 略
    case MyClass2 c2: // ... 略
  }
}

メソッドでディスパッチしたり包括的に処理するかっこいい何かとかを考えたりするよりも、実際問題、これが一番書くのも保守するのもらく😂

C++er には C#generics の貧弱さが…ツライ😅

C#, WPF, XAML: System.Windows.Controls.Control が "いわゆるコントロール" の共通基底だと思い込んで実装したら TextBlock であっさり null 例外に殺された件

// 前提、 C#-7.0
using System.Windows.Controls;
var c = new Control();
if ( c is Label l )
  Console.WriteLine( $"Label.Content={l.Content}" );
if ( c is TextBlock t ) // <--ここでコンパイルエラーが生じてくれる。
  Console.WriteLine( $"TextBlock.Text={t.Text}" );

c is TextBlock t に対し error CS0584: Internal compiler error: constant in type pattern matching が発生し、このコードはコンパイルできません。

てっきり「 XAML で使えるような部品としての意味での "いわゆるコントロール" は Control を共通の基底型に派生している」と思い込んでいました。実際には以下の通り:

TextBlockControl を経由せずに Control の基底にもなっている FrameworkElement から派生していたんですね😂

最初のコード例のような実装ではコンパイラーがエラーを出してくれるのでプログラマーはどんなに間抜けでも実装をリリースできないので安心、見るからにすぐに気づいて修正できる問題に見えます。しかし、これが ItemsControl のデータテンプレートから展開される "いわゆるコントロール"(上記の通り実際には Control とは限らない)を扱う実装を書いてみると、この問題に侵された実装をリリースできてしまう事があります。と、いうかありました😅

// "いわゆるコントロール" を Control だと思い込んだ人の書いたやゔぁいライブラリー実装💀

// ItemsControl から index 番目の ContentPresenter を取得
static public ContentPresenter GetContentPresenter
( ItemsControl control, int index )
{
  return
    control
    .ItemContainerGenerator
    .ContainerFromIndex( index )
    as ContentPresenter
    ;
}

// ContentPresenter で Name の "いわゆるコントロール" を取得
static public T FindControlAs<T>
( ContentPresenter presenter, string name )
  where T : Control
{
  return
    presenter
    .ContentTemplate
    .FindName( name, presenter )
    as T
    ;
}

// ItemsControl に GetContentPresenter + FindControlAs の複合技で
// index 番目の "いわゆるコントロール" を取得
static public T FindControlAs<T>
( ItemsControl control, int index, string name )
  where T : Control
{
  if ( GetContentPresenter( control, index ) is ContentPresenter p )
    return FindControlAs<T>( p, name );
  return null;
}

// ItemsControl の index 番目のアイテム(≃データテンプレート)から
// 複数の Name 群 names にそれぞれ対応した "いわゆるコントロール" を
// 取得して Name => "いわゆるコントロール" の辞書にしてくれる便利機能さん
static public
IDictionary<string, T>
FindControlsAs<T>
( ItemsControl control, int index, params string[] names )
  where T : Control
{
  if ( GetContentPresenter( control, index ) is ContentPresenter p )
  {
    return
      ( from name
        in names
        select FindControlAs<T>( p, name )
      ).ToDictionary( c => c.Name );
  }
  return new Dictionary<string, T>();
}

とか ライブラリー実装しちゃう わけですよ😅

この実装では ItemsControlindex 番目の要素から nameName を持つ要素を探せなかった場合は null を想定していますが、「 ItemsControl から取り出せたアイテムは Control を共通基底型として扱えるハズ」という実装になっているため、実際には Control を基底型としない TextBox などが ItemsControlDataTemplate にある場合にその取得を試みると、アイテムは取得できるのに Control にキャストできないため as キャストで null が保持され、その nullName プロパティーを参照しようとして null 例外落ちする、というような現象が起こります。例えば↑のコード例の終端側から3行前の ToDictionary( c => c.Name ) で例外吐くとか。

ItemsControlDataTemplate の持つ要素に至るまではこの実装形態でのライブラリー側ではコンパイル時にプログラマーが「Control を共通の基底型として使えるハズ」と思い込んでいる状態ではコンパイルエラーやワーニングを吐くように実装できませんから、このライブラリーは使用者の実行時に意図しない例外を吐く上に、使い方によっては「どうしてそこで null 例外が飛んでるの😂」なんて思ってしまうような例外の吐かれ方にもなりえます。

特にライブラリーのリリースビルドだけを使うユーザーにとっては null 例外で落ちる部位で参照しているように見えるオブジェクトは非 null なために "ライブラリーを疑う" 選択肢に気づけないと悩みスパイラルデスマーチにリソースと精神を浪費してしまうかもしれない。しかも出る情報は NullReferenceException だけになったりもする:

f:id:USAGI-WRP:20181226034125p:plain

思い込み、こわいですね😅

今回の ItemsControl の使い方の想定では、 "いわゆるコントロール" は先の派生ツリーでも示した UIElement です。実際問題として LabelTextBox を扱うだけなら FrameworkElement でも意図通りに扱えますが、 ItemsControl のデータテンプレートから ContentPresenter を経由して Name で取得可能な要素は UIElement です。 FrameworkElementUIElement[RuntimeNameProperty( "Name" )] ほか "いわゆるコントロール" っぽいプロパティーを付けたりした "事実上の共通の基底型" です。必要に応じて as りましょう。

おわりに、だいじなこと、もう1回書いとこ。

思い込み、こわいですね😅

( ソフトウェアエンジニアにとって、 null よりも数段、よっぽどこわいものもある。プログラマーの思い込みによる実装は確実にそのうちの1柱だろう。特にコンパイルエラーやコンパイルワーニングをすり抜けてしまうと問題が起こるまでプログラマーは思い込みの上で安心しきり警戒すらしていないかもしれない。)

C#-7.0: 滲み出すスコープ

Visual StudioC# プロジェクトでも ErrorWarning も1件も見逃さない綺麗なビルドを心掛けていますが、 Messages は見落としていました。何件か来ている。

  • f:id:USAGI-WRP:20181223081227p:plain

まず、一番上を見る。 IDE0018: Variable declaration can be inlined とやら。

C#-7.0 で何がどう変わるかを眺めた際に「うへー」って気持ちになった事があったのを思い出してしまいました。

// C# < 7.0
{
  var a = "1.23";
  double b; // <-- 事前に宣言が必要
  var c = double.TryParse( a, out b );
  Console.WriteLine( $"{a} {b} {c}" );
}
// C# >= 7.0
{
  var a = "2.34";
  //double b; <-- 要らんくなった
  var c = double.TryParse( a, out double b ); // <-- out の後で変数宣言していた事にできる言語機能
  Console.WriteLine( $"{a} {b} {c}" );
}

↑手間がかからず一見便利に見えますが、「スコープが滲み出す」とタイトルに書いたような現象が起こるようになります↓

var a = "3.45";
if ( double.TryParse( a, out double b ) )
  Console.WriteLine( $"X: {a} {b}" ); // <-- ここで b が生きているのは感覚的にもまあわかる
Console.WriteLine( $"Y: {a} {b}" ); // <-- コンパイル可能、実行可能、 b はここでも生きている

きもぃ。便利さと拮抗する程度には十分にきもぃ😅

この言語仕様上のスコープがプログラマーソースコード・リーディングにおける視認のスコープ感と明らかに異なるだろう挙動には注意が必要になる。

var p = "3.45";
var q = "4.56";

if ( double.TryParse( p, out double buffer ) )
  Console.WriteLine( buffer );

if ( double.TryParse( q, out double buffer ) ) // <-- コンパイル不能
  Console.WriteLine( buffer );

プログラマー、特に C 系に近い言語を主戦場とするプログラマーにとって、このコードはコンパイル可能な気がすると思います。しかし、 C#-7.0 言語仕様では "正しく" コンパイル不能なコードです。 p のパースの際に out に続けて宣言した double buffer のスコープは if と同じスコープになります。 if の内部でだけ使えるような見栄えのソースコードですが、 if と同じスコープへと double buffer 変数のスコープは "染み出し" しています。

同様の現象は is でも発生します。

var n = new List< int >(){ 1,2,3 };
var m = Enumerable.Range( 10, 20 );

if ( n is IEnumerable< int > integers )
  Console.WriteLine( integers.Count() );

if ( m is IEnumerable< int > integers ) // <-- コンパイル不能
  Console.WriteLine( integers.Count() );

染み出しスコープ言語機能…きもぃ😂 便利なのはワカルけど、どうして if スコープへ束縛とか考えなかったんだろう、言語仕様作った人。

おまけ

他にも Messages は出ているので一応おまけメモ。

  • IDE0033 Prefer explicitly provided tuple element name

↑は↓のようなタプルに名前が付けられる場合にもタプル標準の Item1 とか使っていると出る。

var vs = new List< string >(){ "aaa", "bbb", "ccc" };  
// ↓の t.Item1 に対して IDE0033 が発生する
var xs = ( from v in vs select ( v, v.GetHashCode() ) ).ToDictionary( t => t.Item1, t => t.Item2 );
Console.Write( string.Join( "\n", xs ) );

しかし、このしれっとタプルの変数名を勝手に変更してくれちゃう機能は C#-7.1 かららしい。 .net Framework 4.7.2 とかのプロジェクトでは使えない。 .net Core 2.0 ≃ C# 7.1 らしい。 IDE のおすすめメッセージは良い機能とは思うものの、プロジェクトが使用可能な言語バージョンを超えるサジェストはどうかと思う😂

  • IDE0042 Test C# Variable declaration can be deconstructed

↑は↓のようなタプルを構造化束縛せずに使っている場合に発生する。

var vs = new ( string uni, int tako )[]
{ ( "きたむらさき", 123 )
, ( "えぞばふん", 234 )
, ( "あか", 345 )
};

// ↓タプルを構造化束縛せずに使うと IDE0042
foreach ( var t in patterns )
  Console.WriteLine( $"uni={t.uni} tako={t.tako}" );

// ↓構造化束縛して使うと出ない
foreach ( var ( uni, tako ) in patterns )
  Console.WriteLine( $"uni={uni} tako={tako}" );

WPF,XAML: Image に Source で入れると極端にぼやけたり何かがおかしい JPEG に遭遇してしまった時の簡単な対処方法

仕事で扱ってもいるので国土地理院地理院地図のタイル画像データはよく使わせて頂いています。います、のですが、これがたまにメタデータだったりピクセルフォーマットだったりが奇妙な画像ファイルに遭遇する事がたびたびあります😅

今回は地理院地図のJPEGファイルの一部を WPF の Image コントロールで読ませると極端にぼやけてしまうという怪奇現象に遭遇しました。

https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/17/117000/48132.jpg

↑一見ふつーのJPEGファイルのようにウェブブラウザーやほとんどの画像ビューアー等では表示できるのですが…

<!-- 何かがおかしい JPEG をソースに入れると何かがおかしいレンダリング結果が得られてしまう -->
<Image Height="60" Width="256" RenderOptions.BitmapScalingMode="Fant" Stretch="None" Source="https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/17/117000/48132.jpg"/>

↑これが実行すると↓こんな表示になってしまう😂

f:id:USAGI-WRP:20181221233253p:plain

↑この JPEG ファイルのバイナリー↓からメタデータを解析すると

f:id:USAGI-WRP:20181221234652p:plain

  • 0x0D ( 1 byte ) = 0x01 ( "Density units", 0x01 = "Pixels per inch" )
  • 0x0E ( 2 bytes; littile-endian ) = 0x0001 ( "Xdensity"; Horizontal pixel density )
  • 0x10 ( 2 bytes; littile-endian ) = 0x0001 ( "Ydensity"; Horizontal pixel density )

と、なっていました。「ピクセルの密度単位が 1 pixels/in 」という事は、「1 ピクセルにつき 1 in = 2.54 cm に展開」という事なので、地理院地図のタイル 256x256 pixels は「 6.5 m x 6.5m の表示サイズに展開されるのが正しい」というメタデータになっているのです。なるほど真面目にメタデータを読んじゃったら極端にぼやけるよね(´・ω・`)

<!-- とりあえず簡単な対処方法 -->
<Image Height="60" Width="256" RenderOptions.BitmapScalingMode="Fant" Stretch="UniformToFill" Source="https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/17/117000/48132.jpg"/>

ドットバイドットを期待して StretchNone にしたくなるお気持ちを抑えて UniformToFill を使います。(あるいは ImageWidthHeight の縦横比が画像と同じなら Fill でも期待する表示にできるでしょう😃)

f:id:USAGI-WRP:20181221231804p:plain

XAMLの属性だけで簡単にどうにかできました😋

XAMLではなくソースコード書くぞー的に対応したい場合は、ソース取得時のストリームに介入するか、デコーダーに介入するか、 BitmapImage でデコード後の画像データの分解能情報を妥当な値へ修正するか、ピクセル配列をさらって適当なリソース型へ複製しちゃうなどお好みのレベルで対応できると思います。が、今回は XAML でプロパティー値をちょっと気にするだけで対応できたのでそういうはなしは無しで。

ちなみに、一般的にはパソコンやスマフォなどの一般的なディスプレイ向けのピクセル密度、画像分解能は 96 pixels/in ( DPI=dots/in でもおなじ )に設定されています(特に Windows では長いこと 96 DPI 基準が染み付いています)。 そうしなければならない、という決まりがあるわけではない慣習による値ですが、ヘッダーをてきとーに作ってしまうと今回のように画像ファイルを使用するユーザー側で思わぬトラブルや扱いに手間暇の必要な事態になってしまう事がたまによく起こります😂

96 DPI = 0.01041666666 in/pixel = 0.02645833333 cm/pixel での 256 pixels は 6.77333333333 cm となります。まあ、そんなもんかな、という大きさの感覚で見られるように思います。少なくとも縦横 6.5m はいくらなんでもデータ生成の誤りでしょう。

のちほど、本件は適当な方法で地理院タイルのなかのひとに伝わるように報告しておこうと思います。以前、規則性不明にタイルの一部のPNGファイルが16-Bitピクセルフォーマットで提供されているためにデコーダー、ライブラリーによっては対応できない事がある問題を地理院パートナーネットワークのアンケートで報告した際には数カ月後にしれっと対応して頂けていたし、 HTTP/2 対応なども要望したらいつの間にかしれっと対応、タイルデータの配布も高速にできるよう何かと工夫を進めてくれているので、本件も適当に報告しておけばいつの間にかどうにかしてくれるのではないかな、と期待しています😋

References

C#/.net/NuGet: nuget デビューしたはなし

数年ぶりに C# の実装を書いていて「きぶん」と「おもいつき」で書いてみたライブラリー的な。そのうち整理もするけれど、とりあえず今週末は nuget とかいう何かの author としての使い方の基本を覚える事にしたのです。

Microsoft の解説は、

  1. NuGet パッケージの公開方法 | Microsoft Docs
    • ここを最初に見て「ほーん、パッケージをローカルでこさえたら後はウェブブラウザーでアップロードするのが簡単そうじゃな(とりあえず自動化するわけじゃなきゃAPIキーがどうとか要らん手間じゃしー😋)」
  2. Create and publish a .NET Standard package using Visual Studio on Windows | Microsoft Docs
    • 次にここを見て「うちのプロジェクト、 Package タブないやん(´・ω・`)」
  3. Create and publish a .NET Framework package using Visual Studio on Windows | Microsoft Docs
    • 次にここを見て「ほいほい、ほな nuget ... コマンドどこじゃ…パスも通ってないしNATIVEのCMDでもパス通ってないし、いつものところに入ってそうだけど探すのめんどくさー」
  4. NuGet Gallery | Downloads
    • 次にここへ遊びに行って「VS2017いんてぐれーてっどより new な Windows x86 Commandline の nuget.exe あるじゃないのー、採用♥」

…と、うろうろしながら nuget パッケージを作って nuget.org で owner として公開しました。

手順メモ:

  1. VS2017 Installer で NuGet の…と、いうのは実際問題どうでもよいので気にしない事にして IDE とかいんてぐれーてっどの事は忘れる。
  2. https://www.nuget.org/downloads で最新の Windows x86 Commandline を拾ってくる。ナマ・エグゼをダウンロードするドキドキ体験♥
  3. nuget.exe をどこか適当なところに置いて、端末エミュレーター的な何かから nuget.exe を叩いてサブコマンドリストが出る動作を確認。
    • 私はどうせ Visual Studio で扱う %HOMEPATH%/source/repos/ 以下の dir の .sln の下の .csproj でしか使わないので、この repos にとりあえずで置いた。
  4. nuget.exe spec myproject.csproj して myproject.nuspec を生成
  5. myproject.nuspec を適当に編集
    • 先の Microsoft の案内によれば licenseUrl, projectUrl, iconUrl, releaseNotes, tags を適当に書いてね、との事なので書いた。
      • 先に repos を github へ公開しておいたので licenseUrl, projectUrl はそこへリンクしただけにした。(ただし、どうも licenseUrl をパッケージ外のURLにするのは nuget.org 的には Deprecated ではあるらしい。)
      • iconUrl は Gravater の私のいつものアイコンの URL にしておいた。
    • 加えて、 authors, owners$author$ のままだと nuget.exe がエラーで死ぬので、適当に変数ではない値を書いておく必要があった。
  6. nuget.exe pack して myproject.x.y.z.nupkg をこさえる( myproject.nuspec があるディレクトリーで行う)
  7. ウェブブラウザーhttps://www.nuget.org/packages/manage/upload から myproject.x.y.z.nupkg をアップロードする
  8. README を URL から回収するユーザーインターフェースがあるので github の README.md の raw な URL など貼って取り込ませる。

↓ちなみに・w・

f:id:USAGI-WRP:20181216182231p:plain

nuget.exe は WSL の zsh から叩いても動きます(゜∀。)

UE4/C++: USoundWave と USoundCue または何れにせよ USoundBase でいいやのメモ

UE4 プロジェクト内のアセット化された「なにか」を C++ コードの ctor で拾いたい場合には以下のようにする、するのだが…:

#include "Runtime/CoreUObject/Public/UObject/ConstructorHelpers.h"
// どこかの UObject 系の ctor でプロジェクト内のアセットを引っ張り出す(間違い探しアリ😋)
static ConstructorHelpers::FObjectFinder< USoundCue > my_sound ( TEXT( "SoundWave'/Game/BGMs/game_maoudamashii_3_theme01.game_maoudamashii_3_theme01'" ) );

この実装はコンパイル可能で my_sound に対して特に何も実装を加えていなければうっかり見過ごしてしまうかもしれない。しかし、 UE4 のビルドシステムのログにはしっかりとエラーが報告されている。(少なくとも UE4Editor では reload に失敗するのでほぼ確実に気付くと思う。)

Error: CDO Constructor (MyExperimentalActor): Failed to find SoundWave'/Game/BGMs/game_maoudamashii_3_theme01.game_maoudamashii_3_theme01'

game_maoudamashii_3_theme01 はゲーム系フリー音楽素材の配信元としておなじみの 魔王魂 で入手できる。今回はこの .wav/Game/BGMs に入っていなかった落ちではない。

今回の「間違い」は USoundCue として .wav アセットを探すコードになっている事。 .wav アセットは UE4 的には USoundWave なので:

// .wav を放り込んだだけの USoundWave アセットに対する正しい実装例
static ConstructorHelpers::FObjectFinder< USoundWave > my_sound ( TEXT( "SoundWave'/Game/BGMs/game_maoudamashii_3_theme01.game_maoudamashii_3_theme01'" ) );

と、しなければ期待動作しない。これが UE4Editor で放り込んだ .wavコンテキストメニューから Create Cue するなどして USoundCue 化されたアセットを作ってあるのなら:

// USoundCue に対する正しい実装例
static ConstructorHelpers::FObjectFinder< USoundCue> my_sound ( TEXT( "SoundCue'/Game/BGMs/game_maoudamashii_3_theme01_Cue.game_maoudamashii_3_theme01_Cue'" ) );

となる。

なるのだが、実際問題この段階で USoundCueUSoundWave か何れであるかが問題になる事はたぶん無い。面倒だ・w・

そこで、とりあえず区別する必要の無い段階では USoundBase を使っておけばよい。 USoundBaseUSoundCueUSoundWave の何れの基底型でもある:

// USoundCue でも USoundWave でも USoundBase にしておけばどっちでもいいのです
static ConstructorHelpers::FObjectFinder< USoundBase > my_sound_a ( TEXT( "SoundWave'/Game/BGMs/game_maoudamashii_3_theme01.game_maoudamashii_3_theme01'" ) );
static ConstructorHelpers::FObjectFinder< USoundBase > my_sound_b ( TEXT( "SoundCue'/Game/BGMs/game_maoudamashii_3_theme01_Cue.game_maoudamashii_3_theme01_Cue'" ) );

こうして作った USoundBase は「どちらでもいいや」のまま UAudioComponent::SetSound のパラメーターとして与えられる。(そもそも SetSound の第1引数は USoundBase 型。)

// どこかに適当な UAudioComponent が居るとして
auto c = NewObject< UAudioComponent >( this );
c->SetSound( my_sound_a.Object );

USoundCueUSoundWave 、こやつらの違いは必要な事があれば Cast すればいいし、トリアエズナラスダケ!くらいな程度では USoundBase で取り回していても不便ないしね☺

References

C#: ビルド番号の自動採番の有効化と Properties.Settings.Default の Upgrade トリックの必要について

遠いむかしの記憶、 C# というか Visual Studio のプロジェクト管理システムにはバージョン番号の一部を自動採番する便利な機能があった、ような気がした。実際に執筆時点で現行製品の VS2017 で作った C# のプロジェクトの AssemblyInfo.cs にも次のコメントが自動的に書かれた状態で生成されていた。

// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]

ビルド番号を自動採番にしておくと頻繁な開発とα版、β版の試供などを行うプロジェクトでは何かと便利が良い。そこで、

// 実際に現行の VS2017 で C# プロジェクトを作成し、 AssemblyInfo.cs を
// コメントに従って自動採番ひゃっほーぃ!してみるのだが…
[assembly: AssemblyVersion("0.0.0.*")]

このようなワイルドカードバージョンを定義すると VS2017 は自動生成されていたコメントとは裏腹に 嫌な気配のする赤波線 を引いてエラーを報告し、プロジェクトはビルド不能に陥ってしまう。

Error CS8357 The specified version string contains wildcards, which are not compatible with determinism. Either remove wildcards from the version string, or disable determinism for this compilation

この問題はググるナウい C#er たちが解決方法を教えてくれていた😋

<!-- your-project.csproj を適当なテキストエディターで直接開いていじる -->
<Project ... 略 ... >
  <PropertyGroup>
    ... 略 ...
    <!-- ↓これを true から false へ変える -->
    <Deterministic>false</Deterministic>

さて、それでバージョン番号に自動採番を有効化してみると、今度はソースの微小な変更であれ再ビルドが発生するとバージョン番号が変わるため、 Properties.Settings.Default によりユーザーごとに保存されるはずの設定値がすべて再ビルドを挟む度にプロジェクトの Settings.settings の初期値へと毎度戻ってしまう問題に遭遇した。

この問題の解決方法は stackoverflow でベテラン C#er の Markus Olsson がベストな答えを投稿してくれていた:

  1. Settings.settinfsUpgradeRequiredbool で追加し、既定値を True にしておく。
  2. 設定値をアプリが使う前(例えばメインの Window クラスの構築子とか)に UpgradeRequired をチェックして Upgrade メソッドを呼んで Save もしてしまう。

なるほど😃

なお、この設定値の問題は何れしばらく後にリリース版のアプリのバージョン番号がアップグレードされる際にも発生したであろうので、さっさと気づけて良かったとも思える。