1. ホーム
  2. シーピー

C++のテンプレートとC#の一般化(II) typedef

2022-02-25 23:50:29
<パス

(この記事は少し複雑なので、ゆっくり前後を読んでください)

型定義

C++言語のtypedefは、簡単に言えば魔法のキーワードである。その最も単純な機能は、型の名前を変更し、別名を定義することである。マクロの定義によく似ているが、そうではない。一つは、変数に新しく、覚えやすく、意味のある名前を定義することであり、もう一つは、より複雑な型宣言のいくつかを簡略化することである。しかし、C++のテンプレートでは、型間の受け渡しとバインディングという非常に賢い使い方もできるのです!

##C++ typedef
例を見てみましょう( を使うのは本当に楽しい。 )

template <class T>
class Matrix
{
    class Accessor
    {
       public:
          typedef T DATATYPE;
    }
}

template <typename ACCESSOR>
class Scanner
{
  public:
    typedef typename ACCESSOR::DATATYPE DATATYPE;
}


簡単な使用例としては、以下のようになります。

typedef float DATATYPE;
Matrix<DATATYPE> data; //Data source
Matrix<DATATYPE>::Accessor accessor(data); //accessor
Scanner<Matrix<DATATYPE>::Accessor> scanner(accessor) // retriever


Scanner::DATATYPEはどのようなデータ型ですか?
答えは、floatです!

つまり、上記の場合、2つのテンプレートクラスのデータ型が受け渡され、型が互いに依存しているように見えるのです。
上記のコードは、特別な設計がなければ、次のように書かなければならない。

typedef float DATATYPE;
Matrix<DATATYPE> data;  
Accessor<DATATYPE> accessor(data);
Scanner<DATATYPE> scanner(accessor)


ロジックは理解できても、相互依存関係は慣習によるもので、コード自身がコントロールできるものではありません。
typedefを巧みに利用することで、初めて T->Accessor::DATATYPE->Scanner<ACCESSOR>::DATATATYPE というデータ型は、動作をパスしているように見える。
つまり、typedefによって Scanner<ACCESSOR>::DATATYPE , Matrix<DATATYPE>::Accessor::DATATATYPE , DATATYPE , float はすべて同じ型ですが、異なるエイリアスを持ちます。これらの異なるエイリアスは、異なるセマンティクスを持ち、互いに依存し合っています。

C#の型抜き?

では、さらに質問を進めると、C#はこんなことができるのでしょうか?

まず、C#にはtypedefキーワードもtypenameキーワードもないので、一番わかりやすいコードしか書けないようです
(usingキーワードでエイリアスを定義する機能は弱すぎるので無視してください)

class Matrix<T>
{
    class Accessor
    {
       Accessor(Matrix<T> matrix){}
    }
}
class Scanner<T>
{
    Scanner(Matrix<T>.Accessor accessor){}
}

Matrix<float> data;  
Accessor<float> accessor(data);
Scanner<float> scanner(accessor)


このようなコードは、明らかにプログラマがミスをしないことにのみ依存しており、コード自体の観点からも、何らかの型結合の機能が欠けているのです。

もちろん、C#は強く型付けされているので、このようなコードが間違っているはずはないと思われますし、このように書けば

Matrix<int> data;  
Accessor<float> accessor(data); // it will report an error and indicate that it cannot convert from Matrix<int> to Matrix<float>
Scanner<float> scanner(accessor)


しかし、私が期待するコードは少なくとも

Matrix<float> test = new Matrix<float>();
Matrix<float>.Accessor accessor(data);
Scanner<Matrix<float>.Accessor> scanner(accessor)


C++の設計思想をそのままC#版に適用してみましょう。

一番近いバージョンをあげます。dynamicとwhereというキーワードが使われているので、わからない場合はヘルプを確認してください。

public class AccessorBase
{
    public dynamic value;
}

public class Matrix<T>
{
    public class Accessor : AccessorBase
    {
        public new T value;
    }
}

public class Scanner<Accessor> where Accessor : AccessorBase
{
    public Scanner(Accessor accessor)
    {
        value = accessor.value;
    }
    private dynamic value; 
    public dynamic GetValue() { return value; } 
}


簡略化したテストコードは以下の通りです。

Matrix<float>.Accessor accessor = new Matrix<float>.Accessor();
accessor.value = (float)99.0;
Scanner<Matrix<float>.Accessor> scanner = new Scanner<Matrix<float>.Accessor>(accessor);
dynamic tmp = scanner.GetValue(); //the value of tmp is null, but I want it to be 99.0


問題は、value = accessor.value;という行にあります。AccessorとAccessorBaseの値のうち、1つしか保持できないからです。

whereを使わないのであれば、リフレクションを使わない限りaccessor.valueは呼び出せない。または、dynamic キーワードを使用します。

public Scanner(dynamic accessor)  
{
    value = accessor.value; //but accessor lacks type constraints
}


とにかく、長い間試してみたが、解決しなかった。C言語のtypedef関数に戻ると

C#のtypedef

では、C#でtypedefをモック化できるかというと、次のような結論になります。 そうです!!(笑

コードに直行

public abstract class Typedef<T, TDerived> where TDerived : Typedef<T, TDerived>, new()
{
    private T _value;

    public static implicit operator T(Typedef<T, TDerived> t)
    {
        return t == null ? default : t._value;
    }

    public static implicit operator Typedef<T, TDerived>(T t)
    {
        return t == null ? default : new TDerived { _value = t };
    }
}


これは、あらゆる型の再定義に適合するクラスである。

class DATATYPE : Typedef<double, DATATYPE> { };
class SDATATYPE : Typedef<DATATYPE, SDATATYPE> { }



欠点は、C#が強型であるため、DATATYPEとSDATYPEが全く別のクラスであることです。
ちょっと使いにくい

DATATYPE data = (DATATYPE)99.0;
SDATATYPE sdata = (SDATATYPE)(DATATYPE)99.0;


それをちょっと無視して、前の一般的なコードのどこを変えればいいのかを見てみましょう。

2日間黙々とテストした結果、typedefの操作をモック化しても、まだ型渡しが行われないことがわかりました。

C#はこのようなコードを書くことができないので

typedef typename ACCESSOR::DATATYPE DATATYPE;


typename ACCESSOR::DATATYPE は C コンパイラに ACCESSOR::DATATYPE が型であることを伝えており、今は型がわからないが、それは型であることを表している。
で、C#はそういうのできないから、黒人がいい顔して聞いてくるだけなんだよね。

結局、私は元のコードに戻り、C#の強力な型付けに頼って、お互いの間でデータを受け渡すときに、エラーが発生しないようにしなければなりませんでした:。

class Matrix<T>
{
    class Accessor
    {
       Accessor(Matrix<T> matrix){}
    }
}
class Scanner<T>
{
    Scanner(Matrix<T>.Accessor accessor){}
}

Matrix<float> data;  
Accessor<float> accessor(data);
Scanner<float> scanner(accessor)


ポストスクリプト

記事が完成し、この可能性を探るために3-4日ほど仕事を休んでいました。道筋ははっきりしなかったが、C#のジェネリックとC++のテンプレートコードの設計思想の違いについても理解を深めることができた。たいした成果ではないですが、全体の流れを記事にまとめました。

Typedef<T, TDerived>は思ったほどうまくはいきませんが、typedefの使い方としては非常に効率的で、特にPrimitive型のsealed classであれば継承操作をサポートしているのは特筆すべき点です。

GithubにLikeType, https://github.com/kleinwareio/LikeType という同じような機能を実装した小さなC#ライブラリがあるので、興味のある方は詳しく見てみてください。

終了

最終更新日:2019-12-17
あなたの個人的な技術的な公開番号に従うことを歓迎します:技術オタクは北を指す。