C ++强类型typedef


49

我一直在尝试一种声明强类型typedef的方法,以在编译阶段捕获某些类的错误。通常,我会将int类型定义为几种类型的id,或者将矢量类型化为位置或速度:

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的建议包括使用boost,至少对我而言这是不可能的(至少我确实有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 );
};

实际上,需要在类定义之前对Friend函数进行前向声明,这需要对模板类进行前向声明。

然后,我们记住基本类型的所有成员,只记得它是模板类。

最后,当我们要使用它时,我们将其typedef定义为:

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

现在,这些类型是完全分开的。例如,如果尝试改为向其提供ModelID,则采用EntityID的函数将引发编译器错误。除了必须将基本类型声明为模板之外,还伴随着一些问题,它也相当紧凑。

我希望有人对这个想法有意见或批评吗?

例如,在位置和速度的情况下,编写此书时想到的一个问题是,我无法像以前那样自由地在类型之间进行转换。在将向量乘以标量之前可以得到另一个向量的地方,所以我可以这样做:

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乘以一个时间会产生一个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Јовић

Answers:


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) {  }

如您所见,单元类型存在一些困难。您可以做的一件事是在基本成分上将单位分解为整数指数的向量:

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
嗯,使用模板系统执行操作单元很酷。没想到,谢谢!现在,我想知道您是否可以强制执行例如米与公里之间的转换。
肯坚2014年

@Kian:大概您会在内部使用SI基本单位,即m,kg,s,A,&c。并且为方便起见,只需定义别名1km = 1000m。
乔恩·普迪2014年

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(即使明确要求我强制转换它,也不会阻止我使用错误的变量)。
肯坚2014年

是。就我而言,我不得不阻止自己分配不同种类的ID。比较和算术运算不是我主要关心的。上述构造将禁止分配,但禁止其他操作。
mindriot 2014年

如果您愿意为此投入更多精力,则可以通过使Explicit类包装最常见的运算符来构建一个(也可以)同时处理运算符的通用版本。有关示例,请参见pastebin.com/FQDuAXdu-您需要一些相当复杂的SFINAE构造来确定包装类是否实际上提供了包装的运算符(请参见此SO问题)。请注意,它仍然无法涵盖所有​​情况,可能不值得为此烦恼。
mindriot 2014年

虽然此句法在语法上很优雅,但它将对整数类型产生重大的性能损失。整数可以通过寄存器传递,而结构(即使包含单个整数)也不能传递。
Ghostrider

1

我不确定以下代码如何在生产代码中实现(我是C ++ /编程初学者,例如CS101初学者),但是我使用C ++的宏sys来完成此工作。

#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; } }

注意:请让我知道您想到的任何陷阱/改进。
Noein

1
您是否可以添加一些代码来显示如何使用此宏-就像原始问题中的示例一样?如果是这样,这是一个很好的答案。
杰伊·埃尔斯顿,2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.