C ++で強く型付けされたtypedef


49

私は、コンパイル段階で特定の種類のバグをキャッチするために、強く型付けされたtypedefを宣言する方法を考えていました。多くの場合、intをいくつかのタイプのIDにtypedefするか、位置または速度を指定するベクトルを入力します。

typedef int EntityID;
typedef int ModelID;
typedef Vector3 Position;
typedef Vector3 Velocity;

これにより、コードの意図をより明確にすることができますが、長い夜のコーディングの後、さまざまな種類のIDを比較したり、速度に位置を追加したりするなど、愚かな間違いを犯す可能性があります。

EntityID eID;
ModelID mID;

if ( eID == mID ) // <- Compiler sees nothing wrong
{ /*bug*/ }


Position p;
Velocity v;

Position newP = p + v; // bug, meant p + v*s but compiler sees nothing wrong

残念ながら、厳密に型指定されたtypedefについて私が見つけた提案には、少なくとも私にとっては不可能なブーストの使用が含まれています(少なくともc ++ 11があります)。少し考えてから、私はこのアイデアに思いつき、誰かがそれを実行したいと考えました。

最初に、ベースタイプをテンプレートとして宣言します。ただし、テンプレートパラメータは定義内の何にも使用されていません。

template < typename T >
class IDType
{
    unsigned int m_id;

    public:
        IDType( unsigned int const& i_id ): m_id {i_id} {};
        friend bool operator==<T>( IDType<T> const& i_lhs, IDType<T> const& i_rhs );
};

フレンド関数は、クラス定義の前に実際に前方宣言する必要があります。これには、テンプレートクラスの前方宣言が必要です。

次に、基本型のすべてのメンバーを定義しますが、それはテンプレートクラスであることを覚えているだけです。

最後に、使用したい場合は、次のようにtypedefします。

class EntityT;
typedef IDType<EntityT> EntityID;
class ModelT;
typedef IDType<ModelT> ModelID;

タイプは完全に分離されました。たとえば、EntityIDを受け取る関数は、ModelIDを代わりにフィードしようとすると、コンパイラエラーをスローします。基本型をテンプレートとして宣言する必要があることを除いて、それに伴う問題とともに、かなりコンパクトです。

私は誰かがこのアイデアについてコメントや批評を持っていることを望んでいましたか?

これを書いているときに頭に浮かんだ問題の1つは、たとえば位置や速度の場合、以前ほど自由に型を変換できないことです。ベクトルをスカラーで乗算する前に別のベクトルが得られるので、次のことができます:

typedef float Time;
typedef Vector3 Position;
typedef Vector3 Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t;

強く型付けされたtypedefを使用して、VelocityをTimeでマルチタイピングするとPositionになることをコンパイラーに伝える必要があります。

class TimeT;
typedef Float<TimeT> Time;
class PositionT;
typedef Vector3<PositionT> Position;
class VelocityT;
typedef Vector3<VelocityT> Velocity;

Time t = 1.0f;
Position p = { 0.0f };
Velocity v = { 1.0f, 0.0f, 0.0f };

Position newP = p + v*t; // Compiler error

これを解決するには、すべての変換を明示的に特化する必要があると思いますが、これは面倒です。一方、この制限は、他の種類のエラー(たとえば、このドメインでは意味をなさない、速度に距離を乗算するなど)を防ぐのに役立ちます。だから私は破れ、私の元の問題、またはそれを解決するための私のアプローチについて人々が何か意見を持っているのではないかと思っています。



同じ質問はここにある:stackoverflow.com/q/23726038/476681
BЈовић

回答:


39

これらはファントムタイプのパラメーターです。つまり、表現に使用されるのではなく、同じ表現を持つタイプの異なる「スペース」を分離するために使用されるパラメーター化されたタイプのパラメーターです。

そして、スペースについて言えば、それはファントムタイプの便利なアプリケーションです。

template<typename Space>
struct Point { double x, y; };

struct WorldSpace;
struct ScreenSpace;

// Conversions between coordinate spaces are explicit.
Point<ScreenSpace> project(Point<WorldSpace> p, const Camera& c) {  }

ただし、これまで見てきたように、ユニットタイプにはいくつかの問題があります。できることの1つは、基本的なコンポーネントの単位を整数指数のベクトルに分解することです。

template<typename T, int Meters, int Seconds>
struct Unit {
  Unit(const T& value) : value(value) {}
  T value;
};

template<typename T, int MA, int MB, int SA, int SB>
Unit<T, MA - MB, SA - SB>
operator/(const Unit<T, MA, SA>& a, const Unit<T, MB, SB>& b) {
  return a.value / b.value;
}

Unit<double, 0, 0> one(1);
Unit<double, 1, 0> one_meter(1);
Unit<double, 0, 1> one_second(1);

// Unit<double, 1, -1>
auto one_meter_per_second = one_meter / one_second;

ここでは、ファントム値を使用して、関連するユニットの指数に関するコンパイル時情報でランタイムをタグ付けしています。これは、速度、距離などの個別の構造を作成するよりも優れており、ユースケースをカバーするのに十分かもしれません。


2
うーん、運用にユニットを強制するためにテンプレートシステムを使用するのは素晴らしいことです。考えてなかった、ありがとう!たとえば、メートルとキロメートルの間の変換などを強制できるかどうか疑問に思っています。
キアン14

@Kian:おそらくSI基本単位(m、kg、s、A、&c。)を内部で使用し、便宜上1km = 1000mのエイリアスを定義するだけでしょう。
ジョンパーディ14年

7

いくつかの整数値の異なる意味を区別し、それらの間の暗黙的な変換を禁止したいという似たようなケースがありました。このような汎用クラスを作成しました。

template <typename T, typename Meaning>
struct Explicit
{
  //! Default constructor does not initialize the value.
  Explicit()
  { }

  //! Construction from a fundamental value.
  Explicit(T value)
    : value(value)
  { }

  //! Implicit conversion back to the fundamental data type.
  inline operator T () const { return value; }

  //! The actual fundamental value.
  T value;
};

もちろん、さらに安全にしたい場合は、Tコンストラクタも作成できexplicitます。Meaningそして、このように使用されます。

typedef Explicit<int, struct EntityIDTag> EntityID;
typedef Explicit<int, struct ModelIDTag> ModelID;

1
これはおもしろいですが、十分に強いかどうかはわかりません。typedefed型で関数を宣言すると、正しい要素のみがパラメーターとして使用できるようになります。これは良いことです。ただし、他のすべての用途では、パラメーターの混合を防ぐことなく構文オーバーヘッドが追加されます。比較などの操作を言います。operator ==(int、int)は、文句なしにEntityIDとModelIDを受け取ります(明示的にキャストする必要がある場合でも、間違った変数を使用しないようにしません)。
キアン14

はい。私の場合、異なる種類のIDを相互に割り当てないようにする必要がありました。比較と算術演算は私の主な関心事ではありませんでした。上記の構成は割り当てを禁止しますが、他の操作は禁止しません。
マインドリオット

より多くのエネルギーを投入する場合は、Explicitクラスで最も一般的な演算子をラップすることにより、演算子も処理する(かなり)汎用的なバージョンを構築できます。参照pastebin.com/FQDuAXdu例えば-あなたには、いくつかのかなり複雑なSFINAEは、ラッパークラスは、実際に(参照包まれた演算子が用意されていたか否かを判断するために構築する必要がある。このSOの質問)。気を付けてください、それはまだすべてのケースをカバーすることができず、トラブルの価値がないかもしれません。
mindriot 14

構文的にはエレガントですが、このソリューションでは整数型のパフォーマンスが大幅に低下します。整数はレジスタを介して渡すことができますが、構造体(単一の整数を含む場合でも)は渡すことができません。
ゴーストライダー

1

量産コードで次がどのように機能するのかわかりません(私はC101 /初心者のようなC ++ /プログラミング初心者です)が、C ++のマクロシステムを使用してこれを作り上げました。

#define newtype(type_, type_alias) struct type_alias { \

/* make a new struct type with one value field
of a specified type (could be another struct with appropriate `=` operator*/

    type_ inner_public_field_thing; \  // the masked_value
    \
    explicit type_alias( type_ new_value ) { \  // the casting through a constructor
    // not sure how this'll work when casting non-const values
    // (like `type_alias(variable)` as opposed to `type_alias(bare_value)`
        inner_public_field_thing = new_value; } }

注:考えられる落とし穴や改善点をお知らせください。
ノエイン

1
元の質問の例のように、このマクロの使用方法を示すコードを追加できますか?もしそうなら、これは素晴らしい答えです。
ジェイエルストン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.