C++ 中类型的单位

Units for types in C++

C++ Core Guidlines P.1 change_speed 示例中,它显示了一个 Speed 类型,其使用如下所示:

change_speed(Speed s); // better: the meaning of s is specified
// ...
change_speed(2.3); // error: no unit
change_speed(23m / 10s); // meters per second

我对这个例子的最后两行特别感兴趣。第一个似乎表明,如果您没有为 change_speed 提供任何单位的参数,它将引发错误。最后一行显示使用一些 ms 文字定义的单位。这两个新特性都是现代版本的 C++ 中的吗?如果是这样,如何实现这样的东西,需要什么版本的 C++?

如评论中所述,核心指南中的示例使用用户定义的文字来构造直观表示物理量的特定于应用程序的类型。为了针对特定示例说明它们,请考虑以下类型:

/* "Strong" speed type, unit is always [m/s]. */
struct Speed {
   long double value;
};

/* "Strong" length type, parameterized by a unit as multiples of [m]. */    
template <class Period = std::ratio<1>> struct Length {
   unsigned long long value;
};

跟踪 Length 个对象的单位可能没有多大意义,但对于 Speed 个实例则不是,但让我们在这里考虑最简单的示例。现在,让我们看一下两个用户定义的文字:

#include <ratio>

auto operator ""_m(unsigned long long n)
{
   return Length<>{n};
}

auto operator ""_km(unsigned long long n)
{
   return Length<std::kilo>{n};
}

他们让你像这样实例化 Length 个对象:

/* We use auto here, because the suffix is so crystal clear: */
const auto lengthInMeter = 23_m;
const auto lengthInKilometer = 23_km;

为了构造一个 Speed 实例,让我们定义一个适当的运算符来将 Length 除以 duration:

#include <chrono>

template <class LengthRatio, class Rep, class DurationRatio>
auto operator / (const Length<LengthRatio>& lhs,
      const std::chrono::duration<Rep, DurationRatio>& rhs)
{
   const auto lengthFactor = static_cast<double>(LengthRatio::num)/LengthRatio::den;
   const auto rhsInSeconds = std::chrono::duration_cast<std::chrono::seconds>(rhs);

   return Speed{lengthFactor*lhs.value/rhsInSeconds.count()};
}

现在,让我们再次看一下核心指南中的示例,

void change_speed(const Speed& s)
{
    /* Complicated stuff... */
}

但最重要的是,如何调用这样的函数:

using namespace std::chrono_literals;

int main(int, char **)
{
   change_speed(23_m/1s);
   change_speed(42_km/3600s);
   change_speed(42_km/1h);

   return 0;
}

正如@KillzoneKid 在评论中提到的,此功能需要 C++11。

您的代码中涉及两件不同的事情:

  1. 使用 strong/unit 类型使您的代码更健壮,即,您区分两种整数类型。这在某些语言(例如 Ada)中是内置的,但在 C++ 中不是,但是您可以创建 classes 来包装整数类型以模仿这种行为(见下文)。

  2. 使用运算符文字以用户友好的方式创建此类classes的实例,即,您编写1s 而不是 seconds{1}。这只是一个方便的功能,在某些地方可能会有用。

使用强整数类型非常有用,因为它使您的代码更不容易出错*:

  • 您不能像在现实生活中那样在表示持续时间和长度的类型之间进行转换。
  • 在表示同类事物的类型中(例如,secondshours),如果您失去精度,则不会进行隐式转换,例如,您不能将 seconds 转换为hours,除非你用浮点数类型表示我们的 (float / double)。
  • (隐式和非隐式)转换会为您处理缩放:您可以将 hours 转换为 seconds,而无需手动乘以 3600。
  • 您可以提供实际的运算符来为您进行转换,例如,在您的示例中,长度和持续时间之间的除法运算符可以提供速度。确切的速度类型是根据长度类型和持续时间类型自动推导出来的:
auto speed = 70km / 1h; // Don't bother deducing the type of speed, let the compiler do it for you.
  • 单元类型是自记录的:如果一个函数returns microseconds,你知道这是什么,你不必希望记录返回函数的人 unsigned long long 提到这代表微秒...

* 这里我只说隐式转换,当然你也可以显式转换,比如用duration_cast(丢失精度).


单位类型

"unit" classes 中的包装整数类型一直可用,但 C++11 带来了一种标准的包装整数类型:std::chrono::duration.

A "unit" class 可以定义为:

  • 它代表的事物类型:时间、长度、重量、速度……
  • 它用来表示这些数据的 C++ 类型:intdouble、...
  • 该型与同一单位“1型”的比值

目前,标准仅提供类似持续时间的类型,但已经讨论(也许是提案?)以提供更通用的基本单位类型,例如:

template <class Unit, class Rep, class Ratio = std::ratio<1>> class unit;

...其中 Unit 将是一个占位符,指示所代表事物的种类,例如:

struct length_t { };

template <class Rep, class Ratio = std::ratio<1>>
using length = unit<length_t, Rep, Ratio>;

但这还不是标准的,让我们看看std::chrono::duration:

template <class Rep, class Period = std::ratio<1>> class duration;

Rep模板参数是C++类型:

  • 标准定义的持续时间类型具有整数表示(实现定义)。
  • 基础类型定义了可以隐式进行的转换类型:
    • 您可以隐式地将整数小时转换为整数秒(乘以 3600)。
    • 您可以隐式地将整数秒转换为整数小时,因为这样会丢失精度。
    • 您可以将整数秒转换为 double 小时。

Period 模板参数定义了 duration 类型与一秒(选择的基本持续时间)之间的比率:

  • std::ratio 是一种非常方便的标准定义类型,它简单地表示两个整数之间的比率,并具有相应的操作(*/、...)。
  • 标准提供多种不同比率的持续时间类型:std::chrono::secondsstd::chrono::minutes、...

运算符文字

这些已经在C++11中引入,并且是literals operators

s 是标准的,包含在 chrono 标准库中:

using namespace std::chrono_literals;
auto one_second = 1s;
auto one_hour = 1h;

m 不是标准的,因此应该加上 _ 前缀(因为它是用户定义的),如 23_m。您可以像这样定义自己的运算符:

constexpr auto operator "" _m(unsigned long long ull) { 
    return meters{ull};
}