65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 11 单位与度量

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (22投票s)

2017年5月24日

CPOL

53分钟阅读

viewsIcon

48348

downloadIcon

990

根据测量的单位对数量进行类型化。C++ 11 度量单位数据类型的完整实现。

引言, 背景,
概述,

使用代码,
示例应用程序,
快速参考, 工作原理历史

引言

2014 年 9 月,我在 Code Project 上发表了C++ 中的度量单位类型,其中直接将度量单位用作数据类型,例如:

metres d(50);
secs t (2);
metres_psec v = d / t;
  • 注意:这与以 boost::units 为例的更传统的方法不同,后者语法不那么直接,并且存在一些概念上的不适(参见下面的背景

    LENGTH d = 50*metres;	//metres has been defined as LENGTH * 1
    TIME t = 2*secs;		//secs has been defined as TIME * 1
    VELOCITY v = d / t;
    

它还允许测量相同事物的缩放单位在任何上下文中无缝使用,并通过隐式执行任何所需的转换来确保正确的结果

Kms d(50);
mins t (2);
miles_phour v = d / t;

但当所有参数都是同一有理单位系统(例如 metresKgssecs)的未缩放成员时,会小心不编译任何转换代码(甚至不是“乘以一”),并且会为多个有理单位系统(例如 MKS 和 cgs)执行此操作。

单位定义遵循的协议与我们教科书对单位之间关系的理解非常吻合。

ulib_Base_unit(metres, LENGTH)
ulib_Base_unit(Kgs, MASS)
ulib_Base_unit(secs, TIME)
ulib_Scaled_unit(Kms, =, 1000, metres)
ulib_Scaled_unit(mins, =, 60, secs)
ulib_Scaled_unit(hours, =, 60, mins)
ulib_Compound_unit(metres_psec, =, metres, Divide, secs)//VELOCITY
ulib_Compound_unit(metres_psec2, =, metres_psec, Divide, secs)//ACCELERATION
ulib_Compound_unit(Newtons, =, metres_psec2, Multiply, Kgs)//FORCE
ulib_Compound_unit(Joules, =, Newtons, Multiply, metres)//ENERGY
ulib_Compound_unit(Watts, =, Joules, Divide, secs)//POWER

这里为 C++ 11 提出的新版本在此基础上利用了该语言的新特性。最初的意图仅仅是利用 constexpr 关键字来解决由于 C++11 之前的编译器不支持浮点数作为编译时常量而导致的未解决的运行时性能问题。然而,一件事导致了另一件事,并且它已被完全重写,因为我发现 C++ 11 更能表达我的设计意图。大部分工作都投入到确保其健壮、真正通用、编译最优代码以及使用简单无缝。然而,有一些新功能更为明显

  • 它现在为您定义的每个单位提供字面后缀,因此您可以拥有字面单位类型的常量数量,例如 2_Kms4_mins。这只是语法糖,但它很好而且非常适合。

  • 为 C++11 编写也带来了 constexpr 正确性的新义务,它完全符合。

    constexpr metres dist = 200_metres; //constant quantity
    constexpr secs t = 2_secs;          //constant quantity
    constexpr metres_psec velocity = dist / t; //constexpr expression
    
  • 它提供了一个可选的容差运算符 ^ 用于比较和相等测试

    if(length1 == length2 ^ 0.1_metres)  //if equal within 0.1 metres
        do_something();
    
    这使得测量量的相等性测试更实用,其他比较的含义更精确。
  • 除了确保转换是最优的之外,它还确保不会不必要地执行转换,因此

    sq_metres Area = 3.14159 *  2000_metres * 2000_metres;
    
    将以 metres 为单位进行评估,以产生 sq_metres 的结果,并且
     sq_Kms Area = 3.14159 *  2_Kms * 2_Kms;
    
    将以 Kms 为单位进行评估,以产生 sq_Kms 的结果,而无需任何中间单位的转换。

    但是,如果您将表达式封装为采用指定单位类型的函数,则可能会失去这种灵活性。

    sq_metres area_of_circle(metres radius)
    {
    	return 3.14159 *  radius * radius;
    }
    
    尽管您仍然可以传递 Kmscms 以及 metres 来调用它,但它会通过强制转换为 metres 来处理。为了避免这些不必要的转换,您现在可以定义通用函数,将抽象的度量维度作为参数,而不是特定的单位。
    template<class T>
    auto area_of_circle(LENGTH::unit<T> radius) //radius can be any unit that measures LENGTH
    {
    	return 3.14159 *  radius * radius;
    }
    
  • 对于多种比例单位在乘积中组合在一起且参数多于两个的情况,提供了一个可变参数乘积函数。

    mins time_taken2 = product_of<mins>(2_Kms_pHour, 3_grams , divide_by, 4_PoundsForce);
    
    这将确保在编译时将所有涉及的各种转换合并为整行代码的单个因子。如果以常规方式将其编写为二元运算符链
    mins time_taken = 2_Kms_pHour * 3_grams / 4_PoundsForce;
    
    将导致对三个二元运算符 =*/ 的每个转换进行整合,这不是一回事。当然,结果将同样正确。
  • 为了支持用户界面中的单位类型正确性,它提供了

    一个可以调用以返回任何单位名称作为文本字符串的函数。这使得单位类型可以系统地与其值一起显示。

    一个可以调用以准备组合框的函数,以提供用户可以选择的兼容单位列表以显示单位。

背景

当您处理不同类型的数量或以不同单位测量的相同类型的数量时,需要对数量进行类型化。您希望类型系统阻止不适当的操作并允许适当的操作,确保它们产生正确的结果。不难看出如何创建 metressecs 类并安排它们执行以下操作

metres dist = metres(10); //ok
metres dist = secs(10); //error will not compile

但如果你想让它更好地利用单位的智能使用,那么你还必须强制执行

metres_psec velocity = metres(10) / secs(2);  //ok
metres_psec velocity = metres(10) * secs(2); //error will not compile

您可能会花很长时间为每个单位编写一个类,其中包含它可以与其他单位进行的所有操作。您会发现自己一直在追逐尾巴,发明和编写不熟悉的单位来涵盖所有可能发生的组合。幸运的是,有一种系统地执行此操作的通用方法。

为了能够检查组合单位的正确性,它们的类型需要具有一定的复杂性,并且需要定义它们的组合和比较规则。与许多单位系统一样,该系统的核心是 Scott Meyers 在 http://se.ethz.ch/~meyer/publications/OTHERS/scott_meyers/dimensions.pdf 中描述的 Barton 和 Nackman 的量纲分析方法。这基于表示基本量纲(如长度、质量和时间)幂次的模板参数。

template<int Length, int Mass, int Time> class unit 
{    
    enum {length=Length, mass=Mass, time= Time};   
    double value;     //the data member
 ....
}; 

因此我们可以定义基本的度量维度

using  LENGTH= unit<1, 0, 0>; 
using  MASS = unit<0, 1, 0>; 
using  TIME = unit<0, 0, 1>; 

以及由它们构建的复合测量维度

using  VELOCITY = unit<1, 0, -1>;  // LENGTH/TIME = <1, 0, 0> - <0, 0, 1>
using  ACCERATION = unit<1, 0, -2>;    // VELOCITY/TIME = <1, 0, -1> - <0, 0, 1> 
using  FORCE = unit<1, 1, -2>;    // ACCERATION*MASS = <1, 0, -2> - <0, 1, 0> 

这使得可以检查赋值、比较、加法和减法只能在具有相同维度的单位之间进行,并且乘法和除法产生正确类型的单位。

传统方法,如 boost::units 所例证,使用这些测量维度作为数据类型,并将单位定义为根据这些维度进行缩放的数量。因此单位定义如下

LENGTH metres(1); 	
LENGTH Kms(1000);
TIME secs(1);
TIME hours(3600);

你可以写

LENGTH total_length = 50 * metres + 3 * Kms;
VELOCITY v = 50 * Kms / (5 * hours);

这具有优雅的简洁性,但也存在一个令人不适的概念上的转折,即通过声明米是 LENGTH 为 1,您将 LENGTH 固定为 metres,这很好,但您仍然写 LENGTH。您不能写 metres,因为 metres 不是一个类型,它是一个数量。虽然它可以处理输入到表达式中的缩放单位,但它不适合使用缩放单位。您不能以 Kms 等缩放单位接收结果,因为 Kms 是一个数量,而不是一个类型。如果您将一串 Kms 中的数量相加,那么它们在求和之前将有效地一个接一个地转换为 LENGTH(请记住是 metres)。这有点奇怪,没有机制来传播缩放单位,并且在评估表达式时使用的单位方面缺乏灵活性。

我感到困惑的是,为什么我们不能直接将单位作为数据类型,并以更自然的方式编写代码。于是我着手自己实现,一探究竟。

我很快发现实现它存在一个根本性的困难。如果你有一个像 Kms (= 1000 米) 这样的缩放单位作为类型,那么该类型必须带有一个转换因子,在一般情况下这是一个浮点数,而语言 (C++11 之前) 不允许这样做。实际上,它根本不允许浮点数作为编译时常量。即使是全局 const double 在编译期间也不存在,只有在运行时创建和初始化它的指令。

我真的很想看到这种更自然的语法在实际中使用,并且受到我的经验的鼓舞,即只要你努力,C++ 可以做任何事情,我坚持了下来,并于 2014 年 9 月在 Code Project 上发表了C++ 中的度量单位类型

是的,C++(11 之前)可以做任何事情,除了在编译期间预计算复合转换因子。这意味着一些隐式转换将被编译为一系列因子,这些因子将在每次调用时重新计算。您可以稍加勤奋,指定您希望在程序启动期间预计算某些转换,但我无法安排它自动完成。

单位库应该零开销,并像使用内置数值数据类型编写一样进行编译。一个插入隐式转换的库只有在它插入的代码不多于以最勤奋最优的方式手工编写的代码时才能声称这一点。根据这个标准,它失败了,满足这个标准成为了这里提出的 C++11 重写的必要条件。

这种方法以前可能没有被采用,因为它无法正确完成。使用 C++ 11,它可以正确完成,两个重要的促成因素是

  • constexpr 使得浮点数作为编译时常量和类型信息(静态 const 类成员)成为可能。

  • using = 替换 typedef 以及急需的 template<> using =,这将代码的复杂性降低到可以有效设计的水平。

这就是我在这里介绍的。

概述

基本原理是

定义一些您想使用的单位

#include "ulib_4_11.h"
ulib_Base_dimension1(LENGTH)
ulib_Base_dimension2(MASS)
ulib_Base_dimension3(TIME)

ulib_Begin_unit_definitions

	ulib_Base_unit(metres, LENGTH)
	ulib_Base_unit(secs, TIME)
	
	ulib_Unit_as_square_of(sq_metres, metres)
	
	ulib_Scaled_unit(Kms, =, 1000, metres)
 	ulib_Scaled_unit(mins, =, 60, secs)
 	ulib_Scaled_unit(hours, =, 60, mins)
		
	ulib_Compound_unit(metres_psec, =, metres, Divide, secs)
 	ulib_Compound_unit(Kms_phour, =, Kms, Divide, hours)

ulib_End_unit_definitions

编写一些使用它们的代码

metres width = 10_metres;        //the required width of a road 
sq_metres area = 20000_sq_metres;    //the amount of tarmac available, expressed as coverage
    
Kms length =  area / width;        //the length of road that can be built
secs transit_time = 40_secs;    //time it takes to comfortably drive a car the length

Kms_phour velocity = length / transit_time; //safe velocity in familiar units

首先要注意的是,每个量都由其测量的单位进行类型化。这里选择的单位是您通常用于测量和谈论相关量的单位。因此,道路的宽度以 metres 表示,但长度以 Kms 表示。此外,速度表示为 Kms_phour,但用于计算它的行驶时间表示为 secs。然而,代码中没有显示任何转换因子或函数来处理这个问题。这里的魔法是,必要的转换由所涉及的单位类型隐含,并在编译期间计算并插入到代码中。

您还会看到它接受 Kms_phour 作为 length / transit_time 结果的有效接受者,而如果它是 length * transit_time,它将拒绝并拒绝编译。这当然是您对任何度量单位库所期望的;它允许所有可能的正确组合,但拒绝任何混合不兼容单位的尝试。然而,这个库并不会因表达式需要进行几次转换而感到困惑。每个运算符(在此例中为 /=)将在编译期间识别并合并所需的转换,并仅将一个因子插入到编译代码中。

更复杂的度量单位定义为现有单位的二元组合。以下构建了有理 MKS 机械单位的定义

ulib_Compound_unit(metres_psec, =, metres, Divide, secs)//VELOCITY
ulib_Compound_unit(metres_psec2, =, metres_psec, Divide, secs)//ACCELERATION
ulib_Compound_unit(Newtons, =, metres_psec2, Multiply, Kgs)//FORCE
ulib_Compound_unit(Joules, =, Newtons, Multiply, metres)//ENERGY
ulib_Compound_unit(Watts, =, Joules, Divide, secs)//POWER

定义单位

您首先定义一些基本的度量维度

ulib_Base_dimension1(LENGTH)
ulib_Base_dimension2(MASS)
ulib_Base_dimension3(TIME)

在这里,您会看到熟悉的 LENGTHMASSTIME,因为它是经典且最普遍理解的方法。还有其他不熟悉但连贯且自洽的方法,它们也同样受支持。

您必须定义的第一个单位是代表这些维度的基本单位

ulib_Base_unit(metres, LENGTH)
ulib_Base_unit(Kgs, MASS)
ulib_Base_unit(secs, TIME)

通过这样做,您正在定义主要的基础或有理单位系统——在此例中为 MKS(metresKgssecs)。所有其他单位都将(直接或间接)参照这些基本单位来定义

ulib_Unit_as_square_of(sq_metres, metres) 	//rational
ulib_Unit_as_cube_of(cubic_metres, metres)	//rational

ulib_Scaled_unit(Kms, =, 1000, metres) 	//scaled
ulib_Scaled_unit(mins, =, 60, secs)	//scaled
ulib_Scaled_unit(hours, =, 60, mins)	//scaled

ulib_Unit_as_inverse_of(Herz, secs)		//rational

ulib_Compound_unit(metres_psec, =, metres, Divide, secs)		//rational
ulib_Compound_unit(metres_psec2, =, metres_psec, Divide, secs)	//rational
ulib_Compound_unit(Kms_psec, =, Kms, Divide, secs)		//scaled

基本或有理单位系统也决定了在通过乘法或除法组合具有不同单位类型的数量时评估的工作单位。这保证了,正如预期的那样,其参数都是未缩放有理单位的表达式和子表达式将在没有转换且没有插入任何转换代码的情况下进行评估。

如果单位在参考基本单位时没有缩放,则它们是有理的。在上述单位中,只有 KmsminshoursKms_psec 不是有理单位(Kms_psec 因为它是参照缩放的 Kms 定义的)。这些单位得到安全无缝的处理,但代价是隐式转换。

多重有理单位系统

MKS 并非唯一建立的有理单位系统。还有 cgs(厘米、克、秒),您的代码的某些部分可能更适合用这些单位表示,甚至在底层通过代码中的物理常量或高速仪器读数硬编码到它们。

尽管您可以将 cmsgrams 定义为缩放单位并获得完全正确的结果,但表达式评估会将它们转换为 MKS 有理单位,而您的 cgs 声明又会将它们转换回来,从而导致不必要的转换抖动。相反,您可以将 cgs 注册为次要有理单位系统

ulib_Secondary_rational_unit_systems(cgs)

并声明 cmsgrams 作为其基本单位

ulib_Secondary_base_unit(cgs, cms, =, 0.01, metres)
ulib_Secondary_base_unit(cgs, grams, =, 0.001, Kgs)

secs 已被定义为基本单位(MKS),因此被采用。

ulib_Secondary_base_adopt_base_unit(cgs, secs)

这不是严格必要的,因为默认情况下,除非定义了次要基本单位或明确的采用,否则主要基本单位将用于任何给定维度,并且 secsTIME 的主要基本单位。

定义一些参照它们的 cgs 机械单位

ulib_Compound_unit(cms_psec, =, cms, Divide, secs)
ulib_Compound_unit(cms_pmin, =, cms, Divide, mins)
ulib_Compound_unit(cms_psec2, =, cms_psec, Divide, secs)
ulib_Compound_unit(Dynes, =, cms_psec2, Multiply, grams)
ulib_Compound_unit(Ergs, =, Dynes, Multiply, cms)

现在,以下表达式将无需转换即可评估

cms_psec v = 30_cms / 5_secs;

编译器检测到 cgs 是 cmssecs 的最佳匹配有理单位系统,因此无需转换即可评估 cms_psec 中的结果,然后无需转换为您声明的变量。

多重有理单位系统不仅仅是为了能够符合多种约定。您可以根据自己的需求战略性地发明它们。例如,Kmsmins 可能是人脑最容易理解的航空导航单位。(在下一分钟内行驶 8 公里比 480 公里/小时或 134 米/秒更能提供即时画面)。您可以定义一个有理单位系统并称之为 KmsMins

ulib_Secondary_rational_unit_systems(cgs, KmsMins)

然后将 Kmsmins 定义为其成员

ulib_Secondary_base_unit(KmsMins, Kms, =, 1000, metres)
ulib_Secondary_base_unit(KmsMins, mins, =, 60, secs)

允许它采用 Kgs

ulib_Secondary_base_adopt_base_unit(KmsMins, Kgs)

并定义一个与之配套的有理速度单位

ulib_Compound_unit(Kms_pmin, =, Kms, Divide, mins)

现在你可以将你的变量声明为 KmsminsKms_pmin,并在组合它们时享受无因子评估,例如

Kms_pmin v = 40_Kms / 5_mins; //evaluates without conversions

您还可以在代码的各个级别使用此首选单位,而无需进行转换。在调试时,代码所有级别都使用熟悉的单位非常有帮助。不合理且关键的极端情况值可以更容易地发现。

您可以定义任意多个有理单位系统。每个系统都享有无因子评估的优势。它们共存没有任何问题。它们甚至可以在同一表达式中相遇

Newtons(4)+Kgs(1)*metres(2)/secs::squared(4)+Dynes(50)+grams(500)*cms(20)/secs::squared(3) ;

表达式的整个右侧将无需转换(全部 cgs)即可评估,并产生一个以 cgs 单位 Dynes 表示的中间结果。左侧也将无需转换(全部 MKS)即可评估,并且在将右侧的中间结果(以 Dynes 表示)添加到左侧时只进行一次转换,从而产生以 MKS 单位 Newtons 表示的最终结果。

声明和初始化

定义单位后,您可以声明该单位的数量

metres len;

您可以通过在构造函数中显式传递数值来初始化它

metres len(4.5); //len set to 4.5 metres

但除此之外,它只能通过兼容的单位类型数量(衡量相同事物的数量)进行初始化或赋值

metres len = 4.5_metres; //len set to 4.5 metres
len = 1.2_Kms;	//len set to 1200 metres

这确保您不能在没有看到单位类型名称(在此例中为 metres)紧挨着同一行代码的情况下将数值输入到单位类型中。具体来说,您不能写

len = 1200; //ERROR

这缺少对 len 类型的可见指示,因此会引发错误。

同样,您只能通过调用 as_num_in<unit_type>(quantity) 函数从单位类型中提取数值,该函数要求您明确说明希望接收它的单位。如果它兼容(衡量相同事物),它将转换为您选择的单位,否则将产生编译器错误。

metres length = 200_metres;

double len_in_metres = as_num_in<metres>(length);   // ok - len_in_metres is set to 200
double len_in_Kms = as_num_in<Kms>(length);         // ok - len_in_Kms is set to 0.2

double try_this = as_num_in<secs>(length);          //ERROR - incompatible units

可以对您定义的单位应用一些类型修饰符,如下所示

metres::squared area;
metres::cubed volume;
metres::to_power_of<4> variance_of_area;
sq_metres::int_root<2>  side_of_square;  //gives an error if unit can not be rooted.

它们对于零星使用幂次单位很有用。如果您经常以幂次形式(例如 sq_metres)谈论某种单位类型,那么最好按照您习惯的方式正式定义它,因此

ulib_Unit_as_square_of(sq_metres, metres)
ulib_Unit_as_cube_of(cubic_metres, metres) 

同样,随着 sq_metrescubic_metres 被确立为正式类型,表达它们的临时数学变换变得更容易,例如

cubic_metres::squared variance_of_volume;

这阐明了被测量的数量是一个体积,并且它是平方的,因为它是一个方差。它比 metres::to_power_of<6> 传达了更多信息。

表达式和运算符

表达式通过运算符连接,单位类型量支持所有标准算术和比较运算符,并执行您期望的操作。支持以下操作:

=, +, -, +=, -=%,  %= 与任何可用于衡量相同数量的单位类型。返回类型将是左侧参数的类型。

<, >, <=, >=, ==, !=, 与任何可用于衡量相同数量的单位类型。返回类型将是布尔值。

*,与 任何单位类型。 返回类型将是一个代表组合的新的临时无名有理复合单位。选择的有理单位系统(例如 MKS 或 cgs)将是左侧参数的单位系统。

*,如果两个参数的类型都是相同单位类型的幂。返回类型将是该单位类型提升到两个幂的和或差的幂。

*, /, *=, /= 与 原始数字。 返回类型将是单位类型。
左侧参数为原始数字。 返回类型将是单位类型。
左侧参数为原始数字。 返回类型将是单位类型的倒数。

这些操作的返回类型决定了表达式评估将进行的工作单位。当通过乘法和除法组合不同的单位类型时,它将使用有理单位,否则将避免从您选择使用的单位进行转换。

这意味着以 Kms 表示的长度之和

Kms total_length = 2_Kms + 3* 5_Kms + 4_Kms;

或者一个比较

bool is_bigger = 5_Kms > 3_Kms;

将完全以 Kms 评估,无需任何转换,以下也是如此

sq_Kms Area = 3.14159 * 5_Kms* 5_Kms;

但以下内容

Kms dist_travelled = 5_ Kms_phour * 2_hours;

将以有理单位评估 5_Kms_phour * 2_hours,结果随后需要转换为 Kms。选择的有理单位系统(如果您使用多个)是左侧参数的单位系统,并且对它的转换由一个因子执行。

对未缩放有理工作单位的倾向将强烈感受到,因为许多表达式确实涉及组合不同单位类型以创建新的复合单位。然而,那些不需要这样做的表达式保持您提供的单位将避免在使用缩放单位时进行大量不必要的转换。

不支持与类型化数量无意义的操作。例如,++-- 不能具有独立于其表示单位的含义,因此不支持它们。位运算符也不支持。实际上,位运算符与这种情况如此格格不入,以至于其中一个 ^ 在此上下文中被赋予了完全不同的含义。

容差运算符 ^

运算符 == 在两个数字完全相等时返回 true。对于浮点数来说,这有点碰运气(大多是碰不到)。以至于它实际上只用于查看变量是否以任何方式被干扰。如果你要谈论测量量相等,那么你必须指定在什么容差范围内。这同样适用于运算符 !=,并且运算符 <=>= 如果不指定容差,则几乎无法与 <> 区分开来。

因此,提供了一个容差运算符 ^,可以跟随并修饰任何比较运算符 <, >, <=, >=, ==, !=,如下所示

if( distance1 == distance2 ^ 5_cms) // if distance1 equals distance2 within a tolerance of 5cms.
    do_something();

选择 ^ 作为运算符并非随意。它比比较运算符 <, >, <=, >=, ==, != 具有更低的优先级,并且比布尔逻辑运算符 &&|| 具有更高的优先级。这样它就不需要在条件表达式中使用括号。

if (m == m2 ^ 1_metres || m == m2 ^ 1_metres)
    do_something();

比较运算符 == 将首先读取,然后应用容差运算符 ^,最后布尔 || 将对两个结果进行操作。

所有比较运算符都可以用容差运算符 ^ 进行修饰,并具有以下各自的含义

a ==  b  ^ tolerance        在 tolerance 范围内相等
a != b ^ tolerance          不等于 tolerance
a < b ^ tolerance          小于 tolerance
a > b ^ tolerance          大于 tolerance
a <= b ^ tolerance      小于,或在 tolerance 范围内相等 - 可能在 tolerance 范围内大于
a >= b ^ tolerance        大于,或在 tolerance 范围内相等 - 可能在 tolerance 范围内小于

可变参数 product_of< >(...) 函数

有时,几个以不同缩放单位表示的数量会组合成一个乘积。传统上,这会由某人手动整合所有转换到一个因子中以优化执行。当使用像这样的单位库时,您无法进行这种手动因子调整,因此它应该能够为您完成。当您以链式二进制乘法的方式正常编写乘积时,您将无法获得此功能

mins time_taken2 =  2_Kms_pHour *  3_grams /  4_PoundsForce

因为每个二元运算都是单独编译的,所以没有机会对所有运算进行优化。

相反,调用 product_of<>(...) 可变参数函数,它包含整个乘积,并将在编译期间自动且系统地将所有转换合并为一个因子

mins time_taken2 = product_of<mins>(2_Kms_pHour, 3_grams , ulib::divide_by, 4_PoundsForce);

模板参数(在此例中为 mins)是可选的,允许您声明所需结果的单位。这对于上面的代码行很有用,因为它将最终的 mins 转换与合并的因子一起封装,因此整行代码仅通过单个因子转换即可执行。

ulib::divide_by 参数允许乘积包含商,并应用于其后的所有内容(如同被括号括起来)。尽管您可以多次使用它,但更明智的做法是将其安排为只出现一次,以免解释变得过于混乱。

如果你不指定你想要返回的类型

product_of<>(2_Kms_pHour, 3_grams , ulib::divide_by, 4_PoundsForce);

那么它将以其参数类型中最主要的有理单位系统中的未缩放有理单位返回结果。

基于基准的测量 - 温度

摄氏和华氏温度并非代表一个数量的值,因为它们的零值不代表温度不存在。然而,摄氏和华氏温度的差异是代表一个数量的值,因为它们的零值确实代表温度差异不存在。此外,开尔文是温度的一种测量方式,但它的值确实代表一个数量,因为它的零值代表温度不存在。我们不应忘记,我们并非一直都知道温度不存在是什么意思。

您可能需要不止一次阅读上一段。它涉及温度(摄氏、华氏)、温差(摄氏度和华氏度)和绝对温度(开尔文)的独特身份。以下操作考量应阐明尊重这些独特身份的必要性,以及它们不能被视为同一事物。

  • 摄氏温度和华氏温度之间的转换需要一个因子和一个偏移量。摄氏度和华氏度之间的温差转换只需要一个因子。
  • 摄氏和华氏温度不能直接用于许多算术运算,因为它们的任意基准会产生任意结果,如下所示:
    0 ºC + 0 ºC = 0 ºC
    32 ºF + 32 ºF = 64 ºF,不等于 0 ºC
  • 许多热力学公式使用开尔文作为量,并要求它以量的形式参与算术运算,但对于以摄氏或华氏表示的温度,这样做是不正确的。

所有这些都必须在决定库应提供何种通用能力以涵盖温度及其基于基准的摄氏和华氏度量时加以考虑,包括这种通用能力是否可能应用于其他维度,如 LENGTHTIME。结果如下:

首先,我们认识到温差是一个真正的量,可以定义为单位类型量。因此我们定义了一个新的度量维度 TEMPERATURE

ulib_Base_dimension_4(TEMPERATURE)

并定义 degrees_Cdegrees_F 作为该维度的基本单位和缩放单位

//units of temperature difference
ulib_Base_unit(degrees_C, TEMPERATURE)
ulib_Scaled_unit(degrees_F, =, 0.5555555, degrees_C)

然后我们定义 CelciusFahrenheit 为不同的基准测量类型,首先调用 ulib_Name_common_datum_point_for_dimension 通过描述建立一个它们都可以引用的共同基准点。

//measurements of temperature
ulib_Name_common_datum_point_for_dimension(TEMPERATURE, water_freezes) //All datum measurements of TEMPERATURE will be defined with reference to water freezes  
ulib_datum_Measurement(Celcius, degrees_C, 0, @, water_freezes)  // 0 Celcius at water freezes
ulib_datum_Measurement(Fahrenheit, degrees_F, 32, @, water_freezes) // 32 Fahrenheit at water freezes

基准测量与度量单位完全不同,并支持一套不同且更有限的操作,代表您可以明智地对它们进行的操作。

=                     任何具有相同维度的基准测量(另一个温度)

+, +=                任何具有相同维度的单位类型(增加一个温差)

-,  -=                 任何具有相同维度的单位类型(减少一个温差)

-                        任何具有相同维度的基准测量(返回两个温度之间的差值)

它们在混合单位和度量中的实际应用

Celcius T1 = 32_Fahrenheit; //convert Fahrenheit to Celcius with factor and offset

T1 += 10_degrees_C;          //increase a temperature

Fahrenheit T2 = T1 - 32_degrees_F; //return decreased temperature 

degrees_C temp_diff = T1 - T2;     //return difference between two temperatures

请注意,Celciusdegrees_C 之间没有直接转换(那将是灾难性的),您必须像上面的例子一样通过差异进行转换。

数据度量不能进行幂运算,但它们支持 ::unit 类型修饰符,该修饰符给出它们所测量的单位,因此 Celcius::units 将给出 degrees_C

最后我们来到开尔文。它像摄氏和华氏一样是一种温度测量,但它也是一个真正的量,其零值代表不存在。我们希望它能够在摄氏和华氏之间转换,但我们也希望将其用作一个量,参与热力学表达式中的乘积和商。我们将其定义为相对于我们为摄氏和华氏建立的相同基准的绝对测量。

ulib_absolute_Measurement(Kelvin, degrees_C, 273, water_freezes)

绝对测量支持与上述基准测量相同的操作,此外还支持

*, /                       与任何单位类型数量 - 返回一个新的临时类型数量

*, /, *=, /=      与原始数字 - 返回相同的绝对测量类型

                            左侧参数为原始数字 - 返回相同的绝对测量类型

/                               左侧参数为原始数字 - 返回底层单位的倒数

这使得它可以参与表示真实温度量的数学公式。请注意,即使是开尔文,您也不会被邀请添加两个温度。那会意味着什么?绝对温度在任何意义上都是可加的吗?

绝对测量将隐式转换为其底层单位

degrees_C arg = 293_Kelvin;

底层单位也可以转换为绝对测量,但这必须明确地进行

Kelvin absTemp = Kelvin(293_degrees_C); //affirms that you are reading it as an absolute temperature

数据测量在其他维度中也具有潜在应用。

TIME 维度中,我们将 secs 定义为主要基本单位,这实际上是一个时间差。为了表示时间中的位置,我们将使用日期和时间,它根据您的文化具有不同的基准。这些将是时间维度中的基准测量。在这种情况下,我们仍然没有很好理解的时间不存在的概念,因此没有定义绝对测量的范围。

在建筑中,指定交点有时比指定构件长度更能减少错位,而且使用现代数字测量设备也更方便。这些交点将是相对于固定基准测量的空间点。它们是 LENGTH 维度中的基准测量,需要如此对待,长度仅作为这些基准单位之间的差异出现。在这种情况下,绝对测量的概念对建筑商来说意义不大,但天体物理学家可能会将距地心的距离视为海拔的绝对测量。

为单位类型数量编写函数

任何进行大量工作的代码都不可避免地会将部分工作委托给函数调用。因此,除了能够编写

metres radius = 5_metres;
sq_metres area_of = 3.14159 * radius* radius;

你也会想写

metres radius = 5_metres;
sq_metres area  = area_of_circle(radius);

其中 area_of_circle 封装了计算 3.14159 * radius* radius

如果你将函数定义为接受并返回原始数字(例如 double),则不会获得此结果。

double area_of_circle(double radius)
{
        return PI*radius*radius;
}

从单位类型数量到 double 没有隐式转换,并且使用显式转换协议调用它,as_num_in<metres>(radius)sq_metres 类型的构造,都不方便。

sq_metres area  = sq_metres(area_of_circle(as_num_in<metres>(radius));

相反,我们简单地使用单位类型对函数进行类型化

sq_metres area_of_circle(metres radius)
{
	return PI*radius*radius;
}

这将产生正确的结果,并确保函数不会以错误的单位类型调用,并且其返回值不会被误解。您可以按如下方式调用它

sq_metres area = area_of_circle(5_metres);

您还可以将其与其他测量长度的单位一起使用

sq_Kms area = area_of_circle(5_Kms);

它将隐式将 5_Kms 转换为 5000_metres,因为函数被调用,并且返回值将隐式转换为 area 的声明类型 sq_Kms

这里有一些更多的例子

metres_psec average_speed(metres dist, secs time)
{
	return dist / time;
}

metres distance_moved(metres_psec2 accel, metres_psec init_vel, secs t)
{
	return 0.5 * accel * t* t + init_vel*t;
}

degrees_C max_temperature_diff(Celcius T1, Celcius T2, Celcius T3)
{
	return (abs(T1 - T2) > abs(T2 - T3)) ?
		((T3 - T1) > abs(T1 - T2)) ? (T3 - T1) : abs(T1 - T2)
		:
		((T3 - T1) > abs(T2 - T3)) ? (T3 - T1) : abs(T2 - T3);
}

这是一个递归实现的可变参数函数。

template<typename... Args>
inline metres max_length(const metres& arg, Args const &... args)
{
	auto max_of_rest = max_length(args...);
	return (arg >= max_of_rest) ? arg : max_of_rest;
}
inline metres max_length(const metres& arg)
{
	return arg;
}

如果您在任何密集计算中都遵循 MKS 上下文,那么这是一种非常合适的方法。所有函数都已使用 MKS 单位进行了合理类型化,因此使用 MKS 单位调用它们不会触发转换,并且让缩放单位在涉及任何严肃计算时立即转换为 MKS,这与该方法非常一致。

然而,对于本库所涵盖的更广泛范围,其中缩放单位可以持久化,并且可以使用多个有理单位系统,它们的定义过于狭窄。不希望出现以下情况:

sq_Kms area = area_of_circle(5_Kms);

调用 area_of_circle 时会调用转换为 metres,然后在赋值 area 时再转换回 sq_metes,因为它封装的表达式 PI*radius*radius 将以其传递的任何单位进行评估,无需转换。只是您的函数坚持采用 metres 正在强制进行这些转换。

更糟糕的是,定义了 cms 作为 cgs 有理单位后

cms v = area_of_circle(5_cms);

仍然面临着转换成米再转换回来的问题。

在这两种情况下,都是因为函数坚持以 metres 长度传入,才强制进行了这些不必要且原本可以避免的转换。

我们可以通过更通用地定义函数来接受任何长度单位来消除对 metres 的要求

template<class T>
auto area_of_circle(LENGTH::unit<T> radius) //any unit of length
{
	return PI*radius*radius;
}

此函数将为任何长度单位的半径实例化,并将其传入函数,无需转换。因此

sq_Kms area = area_of_circle(5_Kms);

将实例化它用于 Kms,将调用并评估,无需转换,返回 sq_Kms 中的值。

如果函数有多个输入参数,那么你需要一个模板参数来与每个参数的通用签名关联。

template<class T1, class T2>
auto average_speed(LENGTH::unit<T1> dist, TIME::unit<T2> time ) 
{
	return dist / time;
}

我们已经定义了 LENGTHMASSTIMETEMPERATURE 的基本维度,因为它们是建立所有其他单位的基础。为了方便通用函数定义,定义我们需要的复合维度很有用。这遵循与单位定义相同的逻辑,如下所示:

using AREA = LENGTH::squared;
using VOLUME = LENGTH::cubed;
using FREQUENCY = TIME::inverted;
using DENSITY= MASS::Divide<VOLUME>;

using VELOCITY = LENGTH::Divide<TIME>;
using ACCELERATION = VELOCITY::Divide<TIME>;
using FORCE = ACCELERATION::Multiply<MASS>;
using ENERGY = FORCE::Multiply<LENGTH>;
using POWER = ENERGY::Divide<TIME>;

现在我们可以继续编写更广泛的通用函数了

template<class T1, class T2, class T3>
auto distance_moved(ACCELERATION::unit<T1> accel,
			VELOCITY::unit<T2> init_velocity,
			TIME::unit<T3> t)
{
	return 0.5 * accel * t* t + init_velocity*t;
}

template<class T1, class T2, class T3>
auto max_temperature_diff(TEMPERATURE::measure<T1> t1, 
			TEMPERATURE::measure<T2> t2, 
			TEMPERATURE::measure<T3> t3)
{
	return (abs(t1 - t2) > abs(t2 - t3)) ?
		((t3 - t1) > abs(t1 - t2)) ? (t3 - t1) : abs(t1 - t2)
		:
		((t3 - t1) > abs(t2 - t3)) ? (t3 - t1) : abs(t2 - t3);
}

请注意,如果我们想传递 CelciusFahrenheit 等温度,我们将其定义为 TEMPERATURE::measure<T1>,而不是 unitTEMPERATURE::unit<T1> 将是 degrees_Cdegrees_F

你也可以用可变参数函数做同样的事情

template<class T, typename... Args>
inline auto max_length(const LENGTH::unit<T>& arg, Args const &... args)
{
	auto max_of_rest = max_length(args...);
	return (arg >= max_of_rest) ? arg : max_of_rest;
}
template<class T>
inline auto max_length(const LENGTH::unit<T>& arg)
{
	return arg;
}

通用函数定义在更广泛的上下文中高效,最重要的是,它们保证不会妨碍任何有理单位系统的无因子评估。它们是短函数的选择,这些函数可能在未知和苛刻的上下文中重复执行。如果函数很大并执行大量代码,则避免入口转换的收益会按比例缩小,代码膨胀可能成为问题,因此最好定义固定(非通用)类型函数。

调用非单位类型函数

您可能已经编写了许多函数,并且想要使用它们,但它们通常接受并返回原始数字,通常类型为 double

在许多情况下,您可以轻松地将它们转换为单位类型函数。您只需将输入参数类型化为适当的单位类型,并将返回值和任何局部变量类型化为 auto。这尤其适用于函数短小且封装通用定律的情况,例如上一节中的示例

然而,有许多原因你可能无法这样做

  • 您无法访问函数源代码。
  • 函数的实现涉及单位类型无法表示的数学抽象。

或者可能不想

  • 该函数由其他人维护和更新,因此您不希望您的代码使用未经维护的副本。
  • 您的部分代码仍需要调用原始版本,而您不想维护两个版本。
  • 该函数很复杂但运行可靠,您不想搞乱它。

如前所述,您不能直接将单位类型量传递给接受原始数字的函数。您必须使用 as_num_in<unit_type>(quantity) 显式提取数值,指定您希望它们使用的单位,并且必须确保它们对于被调用的函数是正确的。

为了说明这一点,让我们虚构一个例子

double mass_of_material_required(double area, double height, double density) 
{
    return 1.09 *  area * height * density;
}

其中 1.09 代表工业过程中的浪费,并将在改进后进行调整。这里的关键是,其他人负责更新此函数,并且他们只会这样做。因此,调用他们正在维护的版本而不是您制作的副本非常重要。

您可以直接调用它

Kgs Mass = Kgs(
                mass_of_material_required(
                   as_num_in<sq_metres>(area), 
                   as_num_in<metres>(height), 
                   as_num_in<Kgs_pcubic_metre>(density))
               );

如果您只调用它一次,那没问题。重复使用不仅丑陋冗长,而且必须非常勤奋地确保对 as_num_in<units>(quantity) 的调用请求了正确的单位,并且您从返回值构造了正确的单位类型。做错了会导致错误的结果,而没有警告或错误

因此,编写一个包装器来调用此函数是有意义的。这样,您只需勤奋一次,

Kgs mass_of_material_required( sq_metres area, metres height, Kgs_pcubic_metre density) 
{
    return Kgs(
            mass_of_material_required(
                as_num_in<sq_metres>(area), 
                as_num_in<metres>(height), 
                as_num_in<Kgs_pcubic_metre>(density))
            ) 
}

并在代码中拥有清晰的函数调用。

Kgs Mass = mass_of_material_required(area, height, density);

函数在使用的单位方面可能非常具体和特殊,您应该始终在封装它们之前研究这一点。但是,它们中的许多将属于以下两类之一

  • 它们适用于多个度量维度,并且要求所有输入参数都是同一有理单位系统中的未缩放有理单位,并将返回同一有理单位系统中的未缩放有理单位结果。上面的 mass_of_material_required 函数就是一个例子。
  • 它们仅在一个度量维度中工作,要求所有输入参数都以相同的单位表示,并将以相同的单位返回结果。一个例子如下:

    auto GetMaxTempDiff(Celcius t1, Celcius t2, Celcius t3)
    {
            return degrees_C(
                    get_max_difference_between_three_numbers(
                            as_num_in<Celcius>(t1), 
                            as_num_in<Celcius>(t2),
                            as_num_in<Celcius>(t3))
            );
    }
    它调用了一个数值例程 get_max_difference_between_three_numbers,我们可以想象它可能比我之前编写的 GetMaxTempDiff 函数更优化。

这两个类别都可以进行更通用的定义,但它们需要不同的方法。

GetMaxTempDiff 的通用包装器原型将是

template<class T1, class T2, class T3>
auto GetMaxTempDiff(TEMPERATURE::measure<T1> t1, TEMPERATURE::measure<T2> t2, TEMPERATURE::measure<T3> t3);

但是我们如何实现对 get_max_difference_between_three_numbers 的调用呢?
以下可能看起来简洁整洁

return degrees_C(
                get_max_difference_between_three_numbers(
            //Don't do this!!
                        as_num_in<TEMPERATURE::measure<T1>>(t1), 
                        as_num_in<TEMPERATURE::measure<T2>>(t2), //No don't
                        as_num_in<TEMPERATURE::measure<T3>>(t3)) //No don't
        );

但这是非常错误的。get_max_difference_between_three_numbers 必须接收所有以相同单位表示的输入参数,而上面的代码允许将三种不同的温度测量值直接发送到函数中。以下是正确的

return degrees_C(
                get_max_difference_between_three_numbers(
            
                        as_num_in<TEMPERATURE::measure<T1>>(t1), 
                        as_num_in<TEMPERATURE::measure<T1>>(t2), 
                        as_num_in<TEMPERATURE::measure<T1>>(t3)) 
        );

但冒着被“纠正”的危险,因为它看起来像犯了一个错误。因此,我建议更明确地声明意图;

using working_type = TEMPERATURE::measure<T1>;
return degrees_C(
                get_max_difference_between_three_numbers(
            
                        as_num_in<working_type>(t1), 
                        as_num_in<working_type>(t2), 
                        as_num_in<working_type>(t3)) 
        );

使用第一个参数作为工作类型是一种获得相当不错结果的简单方法,但在所有情况下并非最优。为了获得最佳结果,我们使用可变参数类型推导器 ulib::best_fit_type<...>。因此,一个完全最优的包装器将如下所示

template<class T1, class T2, class T3>
auto GetMaxTempDiff(
        TEMPERATURE::measure<T1> const& t1,
        TEMPERATURE::measure<T2> const& t2,
        TEMPERATURE::measure<T3> const& t3
)
{
        using working_type = ulib::best_fit_type<
                TEMPERATURE::measure<T1>,
                TEMPERATURE::measure<T2>,
                TEMPERATURE::measure>T3>
        >;
        return working_type::units
        (
                get_max_difference_between_three_numbers(
                        as_num_in<working_type>(t1),
                        as_num_in<working_type>(t2),
                        as_num_in<working_type>(t3)
                        )
        );
}

处理多个度量维度的函数不能坚持所有参数都使用相同的单位,但通常会坚持它们都是同一有理单位系统中的未缩放有理单位。为此提供了一个通用协议,如下所示

template<class T1, class T2, class T3>
auto mass_of_material_required(AREA::unit<T1> area,
                        LENGTH::unit<T2> height,
                        DENSITY::unit<T3> density)
{
      enum {
                working_sys = ulib::best_fit_rational_unit_system<
                                        AREA::unit<T1>, 
                                        LENGTH::unit<T2>,
                                        DENSITY::unit<T3>
                                >()
        };

        return MASS::rational_unit<working_sys>
        (
                mass_of_material_required(
                        as_num_in<AREA::rational_unit<working_sys>>(area),
                        as_num_in<LENGTH::rational_unit<working_sys>>(height),
                        as_num_in<DENSITY::rational_unit<working_sys>>(density)
                        )
        );
}

首先,我们必须决定将在哪个有理单位系统中进行评估。ulib::best_fit_rational_unit_system<...>() 将提供与传入类型最匹配的有理单位系统。然后,使用 ::rational_unit<working_sys> 类型限定符,将调用数值函数的所有输入和输出都类型化为该系统中相应维度的有理单位。

数学函数

为方便起见,提供了一些简单的数学函数。

square_of(quantity)cube_of(quantity)inverse_of(quantity)to_power_of<n>(</n>quantity) 将对任何单位类型量进行操作,并以正确的单位返回结果。

sq_metres area = square_of(5_metres);
cubic_metres volume = cube_of(5_metres);
Hertz frequency = inverse_of(0.05_secs);

sqrt(quantity)int_root<n>(</n>quantity) 将对任何其根可以用有效单位表示的单位类型量进行操作

metres len = sqrt(20_sq_metres);
metres len = int_root<3>(20_cubic_metres);
auto res = sqrt(5_metres); //ERROR – no valid unit type for root

这些函数使用任何类型的单位的通用签名。它们未针对数据测量定义,因为它们将是无效操作。

template <class T>
inline constexpr typename ulib::unit<T>::squared square_of(ulib::unit<T> const& u)
{
	return u*u;
}

有些数学过程无法用单位类型量或测量进行,因为它们的中间值无法以单位或测量形式有效表达。因此,执行奇异数学变换的函数可能必须编写为纯数值函数,然后由调用它的单位类型函数进行封装。有一个非常普通的操作会遇到这个问题,那就是求一组温度的平均值。

计算平均温度的正常且最有效的方法是将所有温度相加,然后除以它们的数量。然而,这个库不允许您将温度(摄氏或华氏)相加,因为那样会产生在一般情况下不安全的计算结果。请记住

0 ºC + 0 ºC = 0 ºC
32 ºF + 32 ºF = 64 ºF,不等于 0 ºC

平均温度的概念没有问题。只是将它们相加的中间过程它不允许,也无法实现。有两种解决方法

将温度(CelsiusFahrenheit)转换为 degrees_Cdegrees_F,然后它们可以相加

Celcius average_of_three(Celsius t1, Celsius t2, Celsius t3)
{
	return 0_degrees_C + ((t1  - 0_degrees_C) + (t2  - 0_degrees_C) +(t3  - 0_degrees_C))/3 ;
}

但这增加了可执行代码中的额外减法和加法。

另一种方法是脱离单位类型量的“庇护”,高效地执行计算。

Celcius average_of_three(Celsius t1, Celsius t2, Celsius t3)
{
	return degrees_C ( 
			(as_num_in<Celsius>(t1)
			+ as_num_in<Celsius>(t2)
			+ as_num_in<Celsius>(t3)) / 3
		)
	);
}

无论哪种方式,对于如此简单的需求都带来了很多不便,因此提供了一个可变参数 mean_of (...) 函数,它适用于任何单位或度量类型数量。

Celcius ave_temp = mean_of(t1, t2, t3);

此外,还提供了方差和标准差的可变参数函数。

degrees_C::squared  variance =  variance_of(t1, t2, t3);
degrees_C  deviation =  std_deviation_of(t1, t2, t3);

 

用户界面支持

有两个新功能使度量单位的正确性可以应用到用户界面中

第一个通过提供系统地显示数量所测量单位的方法来提供安全性。它是一个模板函数,将任何单位或测量类型的名称作为以空字符结尾的字符字符串返回。

template <class Unit> constexpr char* get_unit_name();

这可以用于系统地显示或打印单位和度量名称及其测量值。例如,要显示以 metres 测量的数量,显示值将是 as_num_in<metres>(quantity),其单位标签的文本将是 get_unit_name<metres>()

第二个在不破坏安全性的前提下为用户提供了灵活性。它安全地封装了组合框的初始化,为用户提供了选择替代显示单位的选项。

这是一个函数,最好与它一起使用的 lambda 原型一起指定。

ulib::for_each_compatible_unit<ref_unit>
(
	[this](
			char* szName, 
			run_time_conversion_info<ref_unit> converter,
			bool is_ref_unit
		)
	{    //Your code goes here
		//to do - copy szName into combo list

		//to do - store converter so index aligns with string in combo

		//to do - set selection if ref_unit
				
	}
);
</ref_unit>

ref_unit 是您的代码中正在显示的单位类型。对于它在您定义的兼容单位或度量中找到的每个兼容单位或度量,它将调用 lambda 的主体并传递

  • 其名称作为以零结尾的字符串。
  • 一个名为 converterrun_time_conversion_info<ref_unit> 对象
  • 一个布尔值,指示传递的单位是否为 ref_unit

您可以使用该调用来用单位名称填充组合下拉列表,并为每个用户可能做出的选择关联一个 run_time_conversion_info<ref_unit> 对象,以便当选择单位名称时,可以引用其关联的 run_time_conversion_info<ref_unit> 对象。run_time_conversion_info<ref_unit> 对象仅导出两个函数

double to_display_units(ref_unit const& u)

您应该使用它将代码中引用的数量写入屏幕

ref_unit from_display_units(double value_read)

您应该使用它将屏幕上的值读取到代码中引用的数量。每个都会为组合下拉列表中选择的单位提供所需的正确转换。这种方法由示例应用程序中的 MeasurementControl 类进行示例。

使用代码

在定义单位之前,您需要包含标准库中的 <type_traits> 和从本文下载的 "ulib_4_11.h"。如果您想使用 sqrtinteger_root 函数,还需要包含标准库中的 <cmath>。最好有一个单独的头文件来定义您的单位,例如 my_units.h。因此,包含列表将是

#include <type_traits>
#include <cmath>
#include "ulib_4_11.h"
#include "my_units.h"

如果您想将单位定义封装在命名空间中,那么您必须将 "ulib_4_11.h" 包含在相同的命名空间中,但不包括标准库头文件,如下所示

#include <type_traits>
#include <cmath>
namespace my_units
{
	#include "ulib_4_11.h"
	#include "my_units.h"
}

默认情况下,有 7 个维度可供使用。如果您需要更多维度,则必须在 #include "ulib_4_11.h" 之前定义 ULIB_DIMENSIONS,指示维度数量,如下所示:

#define ULIB_DIMENSIONS ULIB_9_DIMS
#include "ulib_4_11.h"

您最多可以指定 15 个维度。如果您想超过这个限制,您必须在 "ulib_4_11.h" 中的 concatenation_macros 中添加一些额外的行。

您也可以指出您需要少于 7 个维度,这将为编译器节省一些工作。本文中的示例仅需要 5 个维度,因此头文件列表如下所示

#include <type_traits>
#include <cmath>

#define ULIB_DIMENSIONS ULIB_5_DIMS
#include "ulib_4_11.h"
#include "my_units.h"

“my_units.h” 头文件应遵循以下协议

定义基本的度量维度

ulib_Base_dimension1(LENGTH)
ulib_Base_dimension2(MASS)
ulib_Base_dimension3(TIME)
ulib_Base_dimension4(TEMPERATURE);
ulib_Base_dimension5(ANGLE);

定义任何次要的有理单位系统

ulib_Secondary_rational_unit_systems(cgs, Kmsmins)

如果没有,可以省略。

开始单位定义

ulib_Begin_unit_definitions
  • 从基本单位开始定义您的单位
    ulib_Base_unit(metres, LENGTH)
    ulib_Base_unit(Kgs, MASS)
    ulib_Base_unit(secs, TIME)
    
    并使用以下宏从它们构建其他单位定义
    ulib_Compound_unit(metres_psec, =, metres, Divide, secs)
    ulib_Scaled_unit(mins, =, 60, secs)
    ulib_Unit_as_square_of(sq_metres, metres)
    ulib_Unit_as_cube_of(cubic_metres, metres)
    ulib_Unit_as_inverse_of(Herz, secs)
    
    ulib_Enable_datum_measures_for_dimension(TEMPERATURE, water_freezes)
    ulib_Datum_measurement(Celcius, degrees_C, 0, @, water_freezes)
    ulib_Absolute_measurement(Kelvin, degrees_C, 273, @, water_freezes)
    
    ulib_Secondary_base_unit(cgs, cms, =, 0.01, metres)
    ulib_Secondary_base_adopt_base_unit(cgs, secs)
    

结束单位定义

ulib_End_unit_definitions

ulib_Begin_unit_definitionsulib_End_unit_definitions 之间定义的所有类型都将构建一个类型列表。目前,此列表仅由用于填充组合框的 ulib::for_each_compatible_unit 函数使用。您可以在 ulib_End_unit_definitions 之后定义单位和测量类型,但它们不会包含在此列表中。此外,在生成此列表时,只会扫描最多 300 行,超出此限制的任何单位都不会包含在内。

为避免全局命名空间中的名称冲突

  • 所有宏都以 ULIB_ulib_ 标志作为前缀
  • 所有库函数要么是全局的,并且被类型化为只接受在此库中定义的单位类型量,要么位于 ulib 命名空间内
  • 所有类型和类型修饰符要么位于 ulib 命名空间中,要么是您定义的单位、度量和度量维度的修饰符。

现在您可以开始使用测量单位对数量进行类型化。

示例应用程序

示例应用程序演示了如何以安全但灵活的方式将单位类型数量引入用户界面。它使用我自己的对话框和控件库 C++ 中的 Windows 对话框设计,无需对话框模板 实现,该库也在 Code Project 上发布。库文件随应用程序提供,不需要资源文件或 IDE 生成的文件。

构建应用程序

它以 Microsoft Visual Studio Express 2015 项目 – units and measures.sln 的形式提供。如果您的 C++ 开发环境不识别 units and measures.sln 文件,只需创建一个新的空项目并复制以下文件:

autodlg.h
autodlg_controls.h
autodlg_metrics_config.h
measurement_ctrl.h
MyUnits.h
ulib_4_11.h
units_gui_examples.h

units and measures demo.cpp

并将 units and measures demo.cpp 设置为要编译的文件。您可能需要调整入口点函数以匹配空项目的入口点函数。它可能是 _tWinMain 之外的其他函数。您可以从项目中删除 .rc 和 resource.h 文件,它们将不会被使用。

单位类型数量的自定义控件

measurement_ctrl.h 包含了用于显示数量值及其单位的通用单位类型复合控件的代码,并允许用户从组合框列表中选择替代显示单位。这展示了如何使用 ulib::for_each_compatible_unit 函数来填充组合框。

	ulib::for_each_compatible_unit<ref_unit>
	(
		[this](
			char* szName, 
			run_time_conversion_info<ref_unit> converter,
			bool is_ref_unit
		)
		{
			// add unit name to combo
			cmbUnit.do_msg(CB_INSERTSTRING, 0, static_cast<wchar_t *>(wchar_buf(szName)));
			//store converter so index aligns with string in combo
			converters.insert(converters.begin(), converter);
			//set selection if ref_unit
			if (is_ref_unit)
				cmbUnit.do_msg(CB_SETCURSEL, 0);
		}
	);
</ref_unit>

并为用户可能做出的每个选择构建和使用一个转换器数组。

std::vector<run_time_conversion_info<ref_unit>> converters;

用于写入和读取显示器

    ref_unit& read()
    {
        int iSel = (int)cmbUnit.do_msg(CB_GETCURSEL);
        if (iSel > -1)
        {
            the_quantity = converters[iSel].from_display_units(_tstof(edtNum.as_text));
        }
        return the_quantity;
    }
    template<class unit>
    void write(unit const& u)
    {
        int iSel = (int)cmbUnit.do_msg(CB_GETCURSEL);
        if (iSel>-1)
        {
            the_quantity = u;
            edtNum.as_text =
                wchar_buf(
                    converters[iSel].to_display_units(the_quantity)
            ); 
        }
    }
对话框

第一个对话框 New_road_dlg 基于 概述 部分描述的道路建设场景。它以最简洁的方式实现,直接从控件读取和写入。

template <class metrics = autodlg::def_metrics>
class New_road_dlg : public autodlg::dialog < metrics, autodlg::auto_size, WS_THICKFRAME>
{
	AUTODLG_DECLARE_CONTROLS_FOR(New_road_dlg)
		//input
		ULIB_MEASUREMENT_CTRL(edtWidth_of_road, at, hGap, vGap, metres)
		ULIB_MEASUREMENT_CTRL(edtCoverage_of_tarmac, 
                     to_right_of<_edtWidth_of_road>, hGap, 0, sq_metres)
		ULIB_MEASUREMENT_CTRL(edtTransit_time, 
                     under<_edtCoverage_of_tarmac>, 0, 2*vGap, secs)
		//output
		ULIB_MEASUREMENT_CTRL(edtLength_of_road, 
                     under<_edtWidth_of_road>, 0, BWidth * 3 / 2, Kms)
		ULIB_MEASUREMENT_CTRL(edtSafe_velocity, 
                     to_right_of<_edtLength_of_road>, hGap, 0, Kms_pHour)

		AUTODLG_CONTROL(btnCalculate, 
         at, BWidth, BWidth * 5 / 4, BWidth, BHeight, BUTTON, BS_NOTIFY | WS_TABSTOP, 0)

	AUTODLG_END_DECLARE_CONTROLS

	void OnInitDialog(HWND hWnd)
	{
		edtWidth_of_road() = 10_metres;
		edtCoverage_of_tarmac() = 20000_sq_metres;
		edtTransit_time() = 90_secs;
		
		edtLength_of_road().read_only();
		edtSafe_velocity().read_only();
		
		btnCalculate.notify(BN_CLICKED);
	}
	void OnNotificationsFrom(_btnCalculate*, UINT NotifyCode, LPARAM lParam)
	{
		if (BN_CLICKED == NotifyCode)
		{
			edtLength_of_road() = edtCoverage_of_tarmac().quantity() / edtWidth_of_road().quantity();
			edtSafe_velocity() = edtLength_of_road().quantity() / edtTransit_time().quantity();
		}
	}
};

这种方法适用于一次性的用户交互式轶事计算。


第二个对话框 Free_fall_dlg 计算在重力作用下下落时的距离和速度。它根据控制时间的量初始化一个数量,就像您准备进行密集计算一样(从控制读取和写入总是很慢)。

void OnNotificationsFrom(_btnCalculate*, UINT NotifyCode, LPARAM lParam)
{
	if (BN_CLICKED == NotifyCode)
	{
		secs t ( edtTime_elapsed());

		edtDistance_fallen() = distance_fallen(t);
		edtVelocity_reached() = falling_velocity(t);
	}
}

它还调用了一些单位类型函数。

constexpr metres_psec2 gravity(9.8);

metres distance_fallen(secs const& t)
{
	return 0.5 * gravity * t* t;
}
metres_psec falling_velocity(secs const& t)
{
	return gravity * t;
}

类型固定为 MKS,因为内部引用的常量 gravity 硬编码为 MKS。


第三个对话框 Mass_and_energy_dlg 处理任何加速物体,并进行力与能量分析和协调。在这种情况下,输入值在分析中多次使用,为避免重复读取控件,其数量被分配给简单的单位类型变量。简单的单位类型变量也用于保存将进一步多次使用的中间值。最后,输出控件根据使用这些中间值的计算进行分配。

void OnNotificationsFrom(_btnCalculate*, UINT NotifyCode, LPARAM lParam)
{
	if (BN_CLICKED == NotifyCode)
	{
		secs t (edtTime_elapsed());
		metres_psec2 a(edt_Acceleration());
		Kgs m( edt_Mass_moved());

		metres d = distance_moved(a, 0_metres_psec,  t);
		metres_psec v = velocity_reached(a, 0_metres_psec, t);
		Newtons f = a * m;

		edtDistance_moved() = d;
		edtVelocity_reached() = v;
		edtKinetic_energy() = 0.5 * m * v * v;
		edtForce_required() = f;
		edtEnergy_consumed() = f * d;
		edtPower_required() = f * v;
	}
}

在这种情况下,中间值是通过调用泛型定义的函数进行评估的。

template<class T1, class T2, class T3>
auto distance_moved(
	ACCELERATION::unit<T1> accel,
	VELOCITY::unit<T2> init_velocity,
	TIME::unit<T3> t)
{
	return 0.5 * accel * t* t + init_velocity*t;
}
template<class T1, class T2, class T3>
auto velocity_reached(
	ACCELERATION::unit<T1> accel,
	VELOCITY::unit<T2> init_velocity,
	TIME::unit<T3> t)
{
	return accel * t + init_velocity;
}

您可能会注意到动能是就地评估的。您也可以这样做。您不必调用函数。


第四个对话框 Rotational_power_dlg 说明了一种用于旋转系统的不传统单位定义方法。传统观点认为,角度(以弧度表示)是两个长度之间的比率,因此是一个无量纲的标量。这种形而上学的断言与我们测量角度且以不同方式测量的事实不符,它也导致能量(1 牛顿移动 1 米)和扭矩(1 牛顿作用于 1 米半径)之间存在非常容易出错的维度同一性。这种混淆 arises 是因为 1 米在能量定义中扮演的角色与在扭矩定义中扮演的角色非常不同。一个是力移动的距离,另一个是力作用的半径。

我的感觉是这并没有正确地体现角度的作用,并且半径米(几何属性)和作用线上的米(所走的距离)之间缺乏区别。我决定创建一个名为 ANGLE 的维度,并将半径米称为 radial_metres,然后弄清楚它们应该如何关联才能使一切正常工作。

C++ 中的度量单位类型中,我通过寻找能量的维度相等性来解决这个问题,这是我想要的结果。然而,沿作用线的距离、转过的角度和径向距离之间的关系是一个简单的几何关系。

沿作用线的距离 = 转过的角度 x 径向距离

所以重新排列表达式来表示径向距离

径向距离 = 沿作用线的距离 / 转过的角度

如果我们肯定沿作用线的距离具有 LENGTH 的维度,那么径向距离必须具有 LENGTH / ANGLE 的维度

所以我们添加一个 ANGLE 维度

ulib_Base_dimension5(ANGLE);

以及一个 radians 的主要基本单位

ulib_Base_unit(radians, ANGLE)

和我们对 radial_metres 的定义

ulib_Compound_unit(radial_metres, =, metres, Divide, radians)//RADIAL_LENGTH

现在我们可以正确定义扭矩了

ulib_Compound_unit(Nm_torque, =, Newtons, Multiply, radial_metres)//TORQUE

如果您进行量纲分析,您会发现这将使扭矩转动一个角度,其量纲与力作用于距离的量纲相同。它们在能量上具有量纲相等性。

这个例子很简单。它计算给定发动机转速和保持该速度所需的制动扭矩的功率输出。它使用以下公式:

功率 = 扭矩 * 角速度。因此它定义了输入控件

ULIB_MEASUREMENT_CTRL(edtRotational_velocity, at, hGap, vGap, radians_psec)
ULIB_MEASUREMENT_CTRL(edtBraking_torque, under<_edtRotational_velocity>,
			0, BHeight, Nm_torque)

和一个输出控制

ULIB_MEASUREMENT_CTRL(edtPower_output, under<_edtRotational_velocity>,
			0, BWidth * 3 / 2, Watts)

计算为两个输入数量的乘积。

void OnNotificationsFrom(_btnCalculate*, UINT NotifyCode, LPARAM lParam)
{
	if (BN_CLICKED == NotifyCode)
	{
		edtPower_output() = edtBraking_torque().quantity() * edtRotational_velocity().quantity();
	}
}

尽管 Wattsradians_psec 是有助于清晰理论分析的单位(您可以思考它们而不必担心因子),但记录测量值的人员可能会更习惯使用每分钟转数和马力。因此,revolutionhorsepower 被定义为缩放单位。

ulib_Scaled_unit(revolutions, =, 2 * 3.14159, radians)
ulib_Compound_unit(revs_pmin, =, revolutions, Divide, mins)
ulib_Scaled_unit(HorsePower, =, 735.499, Watts)

因此,它们将自动出现在相应的组合框单位选择列表中。


最后,Units_of_measurement_input_output_demo 是一个选项卡式对话框,包含并显示上述对话框。

快速参考

单位定义宏
请参见定义单位多重有理单位系统
ulib_Base_unit(new_unit_name, base_dimension)
base_dimension 必须是基本维度,而不是复合维度。
ulib_Base_unit(metres, LENGTH)

ulib_Compound_unit(new_unit_name, =, left, operation, right)
operation 可以是 Multiply 或 Divide。leftright 可以是任何单位类型。
ulib_Compound_unit(metres_psec, =, metres, Divide, secs)
ulib_Compound_unit(Newtons, =, metres_psec2, Multiply, Kgs)

ulib_Unit_as_power_of(名称, =, 原始, P) 及其衍生。
原始可以是任何单位类型 – P必须是整数。
ulib_Unit_as_square_of(sq_metres, metres)
ulib_Unit_as_cube_of(cubic_metres, metres)
ulib_Unit_as_inverse_of(Herz, secs)

ulib_Scaled_unit(名称, =, unit_as_orig_units, 原始)
原始可以是任何单位类型。
ulib_Scaled_unit(mins, =, 60, secs)

ulib_Secondary_base_unit(次级系统, 新单位名称, =, _因子, 现有基本单位)
现有基本单位必须是一个未缩放的现有基本单位。
ulib_Secondary_base_unit(cgs, cms, =, 0.01, metres)

ulib_Secondary_base_adopt_base_unit(次级系统, 现有基本单位)
现有基本单位必须是一个未缩放的现有基本单位。
uulib_Secondary_base_adopt_base_unit(cgs, secs)
基准测量定义宏
参见 针对基准的测量
ulib_Name_common_datum_point_for(维度, 基准点名称)
基准点名称应是一个描述性标识符。
ulib_Name_common_datum_point_for(TEMPERATURE, water_freezes)

ulib_Datum_measurement(名称, 现有单位, _偏移, 在, 基准点名称)
基准点名称必须是为现有单位的维度定义的名称。
ulib_Datum_measurement(Celcius, degrees_C, 0, @, water_freezes)
ulib_Datum_measurement(fahrenheit, degrees_F, 32, @, water_freezes)

ulib_Absolute_measurement(名称, 现有单位, _偏移, 在, 基准点名称)
基准点名称必须是为现有单位的维度定义的名称。
ulib_Absolute_measurement(Kelvin, degrees_C, 273, @, water_freezes)
类型修饰符
对于单位类型 - 参见 声明和初始化
metres::squared
metres::cubed
metres::to_power_of<4>
sq_metres::int_root<2>

对于基准测量类型 - 参见 针对基准的测量
Celcius::units //units in which measurement is made (degrees_C)

对于测量维度 - 参见 为单位类型量编写函数

using AREA = LENGTH::squared;
using VOLUME = LENGTH::cubed;
using FREQUENCY = TIME::inverted;
using VELOCITY = LENGTH::Divide<TIME>;
using FORCE = ACCELERATION::Multiply<MASS>;
全局函数
as_num_in<type>(quantity_or_measure)</type>. - 参见 声明和初始化
从任何单位类型量或基准测量中提取数值。
如果兼容(测量相同的事物)将转换为请求的单位,否则抛出编译器错误。
double numerical_value_in_Kms = as_num_in<Kms>(5500_metres); 

abs(quantity) 返回绝对正值。
metres absolute_value = abs(-5_metres);

幂和根 - 参见  数学函数
sq_metres Area = square_of(5_metres);
cubic_metres Volume = cube_of(5_metres);
cubic_metres Volume = to_power_of<3>(5_metres);

metres side_of_square = sqrt(25_sq_metres); 
//fails if operand is not a squared type
metres edge_of_cube = integer_root<3>(625_cubic_metres) 
//fails if operand is not rootable type

可变参数 product_of<>(...) 函数 - 参见 可变参数 product_of< >(...) 函数
优化不同缩放单位中量的多个乘积。
可以接受任意数量的参数,可以是任何单位类型或数字。
mins time_taken2 = product_of<mins>(2, 2_Kms_pHour, 3_grams , divide_by, 4_PoundsForce);
metres dist = product_of<>(2_Kms_pHour, 30_mins, 10.0, divide_by, 2);

可变参数统计函数。这些函数将同时接受单位类型量和基于基准的测量。
  - 参见  数学函数

Celcius average_temperature = mean_of(120_Celcius, 230_Fahrenheit, 300_Kelvin ); 
//because you can't add temperatures

metres::squared variance = variance_of(12.5_metres, 13.3_metres, 11.6_metres);

metres standard_deviation = std_deviation_of(12.5_metres, 13.3_metres, 11.6_metres);
泛型函数定义
参见 为单位类型量编写函数
在以下范围内 泛型签名
template<class Tn> LENGTH::unit<Tn> //任何测量长度的单位
template<class Tn> TEMPERATURE::measure<Tn> //任何温度的基准测量
template<class T> ulib::unit<T> //任何单位类型
template<class T> ulib::measure<T> //任何基准测量类型
对于数值函数的包装器
参见 调用非单位类型函数
ulib::best_fit_type<...> 提供参数列表中最多数(左偏)的类型。
当所有测量相同的事物但单位可能缩放不同时,用于确定最合适的工​​作类型。
	using working_type = ulib::best_fit_type<
                TEMPERATURE::measure<T1>,
                TEMPERATURE::measure<T2>,
                TEMPERATURE::measure>T3>
	
ulib::best_fit_rational_unit_system<...>() 返回参数列表中最合适的有理单位系统。
当处理的类型测量不同事物时使用。
  enum {
                working_sys = ulib::best_fit_rational_unit_system<
                                        AREA::unit<T1>, 
                                        LENGTH::unit<T2>,
                                        DENSITY::unit<T3>
                                >()
        };
	
维度::rational_unit<sys> 提供给定维度在给定有理单位系统中的有理单位。
LENGTH::rational_unit<working_sys>;
VELOCITY::rational_unit<working_sys>;
	

工作原理

如果你真的想知道它是如何工作的,那么你必须检查代码。注释不多,但代码整洁。我将在此通过概述其内部架构的主要支柱来提供一些指导。

尺寸

Barton 和 Nackman 的量纲分析方法被一个抽象的泛型 dimensions<> 类封装。

template <int d1, int d2, int d3 .....>
struct dimensions
{
	enum {D1 = d1, D2 = d2, D3 = d3 .....};

	….......
}

当你定义一个基本维度时

ulib_Base_dimension1(LENGTH)

你将 LENGTH 定义为 dimensions<> 的特化。

using  LENGTH = dimensions<1, 0, 0, ….>;

LENGTH 和你定义的任何其他测量维度都不是数据类型。它们是仅在编译期间引用的抽象类型。

单位类型量

所有单位类型量的数据类型是模板类 ulib::unit<>。这是它的原型。

template <
		class T, //description class of unit
		class dims = typename T::dims,
		int Sys = T::System
	> class unit; 

是类 T(单位描述类)决定它代表哪个单位。

当你定义一个单位时

ulib_Base_unit(metres, LENGTH)

生成以下代码

struct  _metres
{
	using dims = LENGTH;
	enum { System = 1};

	enum { is_conv_factor = 0};
	static constexpr double to_base_factor=1.0;

	using base_desc = _metres;
	enum { power = P};
};
using metres = ulib::unit<_metres>;	

定义一个带有混淆名称 _metres 的描述类,并将你选择的名称 metres 定义为 ulib::unit<>,将描述类作为其第一个模板参数传递。

当你定义一个缩放单位时

ulib_Scaled_unit(feet, =, 0.3048, metres)

生成以下代码

struct  _feet
{
	using dims = _metres::dims;
	enum { System = _metres::System};

	enum { is_conv_factor = 1};
	static constexpr double to_base_factor=0.3048;

	using base_desc = _feet;
	enum { power = 1};
};
using feet = ulib::unit<_feet>;	

描述类包含有关正在定义的单位的编译时信息。

  • dims 保存它测量的 dimensions<>

    System 保存一个整数,表示与其关联的有理单位系统。

    is_conv_factor 为零表示没有转换为基本单位,非零表示有。这提供了需要转换的独立逻辑指示。

    to_base_factor 将转换因子作为 static constexpr double 类成员保存。

    base_descpower 是保持单位类型幂次标识的机制的一部分。

必须“动态”定义描述类并将其传入是不可避免的。尽管 C++11 现在允许 static constexpr double 类成员,但它仍然不允许浮点数作为模板参数。这意味着转换因子永远不能作为模板参数传入 ulib::unit。你必须编写一个新的类,其中因子用字面值初始化,然后将其传入。一旦确定了这种类型间接的必要性,将所有单位特定信息放入这些类中就有设计优势。最重要的是,这意味着 ulib::unit<T> 完全由其第一个模板参数定义,其他参数默认使用它提供的值。class dims = typename T::dimsint Sys = T::System 模板参数用于在重载解析期间对维度和有理单位系统进行类型过滤。

template <class T> function_taking_any_length(ulib::unit<T, LENGTH> arg);

在用户级别,你看到同样的事情以不同的方式表达。

template <class T> function_taking_any_length(LENGTH::unit<T> arg);

 

转换

描述类的 is_conv_factorto_base_factor 成员提供了确定是否需要转换(如果不需要则不编译)以及转换应如何进行所需的信息。这是通过以下机制完成的。

template<class T, bool>  struct convert_to_base_imp_s
{
	inline double convert(double value)
	{
		return  T::to_base_factor * value;
	}
}
template<class T>  struct convert_to_base_imp_s<false>
{
	inline double convert(double value)
	{
		return  value;
	}
}
template <class T> double convert_to_base(double value)
{
	convert_to_base_imp_s<T, T::is_conv_factor>::convert( value);	
}

convert 的两个版本都将内联编译,但结构体 convert_to_base_imp_s<T, false> 中的版本不执行任何操作,因此调用它的 convert_to_base 也不执行任何操作。结果是,它们的内联编译将为空,就好像这些调用从未存在过一样。通过这种方式,在不需要的地方,运行时转换代码被完全消除。

这巧妙地封装了缩放单位与其缩放来源的基本单位之间的转换,并隐含了从相同基本单位缩放的不同单位之间的转换。然而,这个库支持多个有理单位系统,这并不那么简单。

当支持多个有理单位系统时,从一个单位到另一个单位的转换可能涉及

  • 从缩放单位到其基本单位的转换,
  • 从一个有理单位系统到另一个有理单位系统的转换,可能包含多个维度。
  • 以及从该有理单位系统的基本单位到缩放单位的转换。

这三个组件由模板结构 unit_to_unit<> 管理。

template <class UFrom, class UTo> struct unit_to_unit:

unit_to_unit<> 仅包含编译时常量 is_conv_factorfactor

static constexpr double factor = 
	UFrom::to_base::factor
	* Sys2Sys<
		UFrom::dimensions, UFrom::unit_sys, UTo::unit_sys
	>::factor
	/ UTo::to_base::factor;

enum {
	is_conv_factor = (1.0== factor)? 0 :
		(std::is_same<ufrom, uto="">::value != 0) ? 0
		: UFrom::to_base::is_conv_factor
		+ Sys2Sys<
            UFrom::dimensions, UFrom::unit_sys, UTo::unit_sys <ufrom::dimensions, ufrom::unit_sys="">
		>::is_conv_factor
		+ UTo::to_base::is_conv_factor
	};
</ufrom::dimensions,></ufrom,>

这两个常量将上面描述的三个转换组件链接在一起,这些组件编码为为每个单位定义的类 to_base 和类 Sys2Sys<UFrom::dimensions, UFrom::unit_sys, Uto::unit_sys>,我将稍后描述。这些类本身也包含 is_conv_factorfactor 常量。

factor 常量由三个组件的 factor 成员按预期方式组成。

然而,is_conv_factor 是通过将三个组件的 is_conv_factor 成员相加而形成的。如果任何组件具有非零的 is_conv_factor,则结果 is_conv_factor 将是非零的,表明必须使用 factor 来执行转换。但是,如果它们都为零,则结果 is_conv_factor 将为零,表明不需要转换。如果以下情况,此评估将被绕过,并将 is_conv_factor 设置为零:

  • factor 评估为 1.0 (1.0== factor)? 0 : 尽管作为主要操作模式不可靠,但此测试在实践中可以实现一些因子的抵消。
    或者 UfromUto 相同 (std::is_same<ufrom, uto="">::value != 0 ? 0 :</ufrom,>

在代码中,您会发现 factor 常量的额外间接,这确保了一旦确定了零 is_conv_factor,其 factor 成员将始终读取为精确的 1.0。

static constexpr double factor = std::conditional_t
	<
		is_conv_factor != 0,
		with_factor,
		no_factor_base
	>::factor;

这确保了在 factor 的正常评估中出现的错误漂移不会污染其作为标识因子的身份。

模板结构 Sy2Sys<> 处理任何测量维度(LENGTHTIMEVELOCITY 等)从一个有理单位系统到另一个有理单位系统的转换。它的模板参数是:一个 dimensions 结构和两个表示两个有理单位系统的整数。

template <class dims, int from_sys, int to_sys> struct Sys2Sys;

Sys2Sys<> 的特化被定义用于处理其应用于有理单位系统与其自身之间的转换,作为非转换。

template <class dims, int Sys> struct Sys2Sys<dims, Sys, Sys>
			: public no_factor_base
		{};

当只有一个有理单位系统时,这将是唯一需要的特化,它在 unit_to_unit<> 实现中的作用是表示因子为 1.0 且无需转换。因此,unit_to_unitfactor 评估将像这样编译:

static constexpr double factor =	Ufrom::to_base::factor / UTo::to_base::factor;

Sys2Sys<> 的通用实现将其 factor 常量定义为

static constexpr double factor = 
	#define ULIB_ENUMERATED_TERM(n)	\
	(					\
		(dims::D##n != 0) ?		\
			int_power<dims::D##n>	\
				::of(Base2Base4Dim<n, from_sys="">::factor)\
	: 1)
	ULIB_CONCAT_ENUMERATED_TERMS_FOR_ALL_DIMS(*)
	#undef ULIB_ENUMERATED_TERM	
	;
</n,>

对于 5 个维度扩展为

static constexpr double factor = 
	((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys, to_sys>::factor): 1)
	* ((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys, to_sys>::factor): 1)
	* ((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys, to_sys>::factor): 1)
	* ((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys, to_sys>::factor): 1)
	* ((dims::D1 != 0) ? int_power<dims::D1>::of(
		Base2Base4Dim<n, from_sys="">::factor): 1);
</n,>

Base2Base4Dim<> 表示两个有理单位系统之间基本维度(如使用 ulib_Base_dimension$ 宏定义的 LENGTHMASSTIME)的转换。

Sys2Sys<> 根据传递给它的 dimensions<> 结构构建一个复合转换。它的模板参数是一个表示基本维度的整数和两个表示两个有理单位系统的整数。

template <int Dimension, int from_sys, int to_sys>
		struct Base2Base4Dim;

Base2Base4Dim<> 的一个特化被定义为处理其应用于有理单位系统与其自身之间的转换,作为非转换。

template <int Dimension, int Sys>
		struct Base2Base4Dim<Dimension, Sys, Sys>
			: public no_factor_base
		{};

其通用实现是

template <int Dimension, int from_sys, int to_sys>
struct Base2Base4Dim
{
	static constexpr double factor =
		Base2Base4Dim<Dimension, from_sys, 1>::factor
		*Base2Base4Dim<Dimension, 1, to_sys>::factor;
	enum {
		is_conv_factor = (1.0 == factor)? 0 :
		Base2Base4Dim<Dimension, from_sys, 1>::is_conv_factor
		+ Base2Base4Dim<Dimension, 1, to_sys>::is_conv_factor
};

这依赖于在定义次级基本单位时定义的 Base2Base4Dim<> 的特化。例如,当你编写

ulib_Secondary_base_unit(cgs, cms, =, 0.01, metres)

生成以下 Base2Base4Dim<> 特化:

template <>		
struct ulib::ung::factors::Base2Base4Dim<1, 2, 1>	
{	
	enum { is_conv_factor = 1 };
	static constexpr double factor = 0.01;
};	
template <>	
struct ulib::ung::factors::Base2Base4Dim<1, 1, 2>
{
	enum { is_conv_factor = 1 };
	static constexpr double factor = 100;
};	

第一个模板参数 1 代表 LENGTH 的基本维度,另两个代表两个有理基本系统。为每个转换方向生成一个 Base2Base4Dim<>

同样,当你写作

ulib_Secondary_base_unit(cgs, grams, =, 0.001, Kgs)

生成以下 Base2Base4Dim<> 特化

template <>		
struct ulib::ung::factors::Base2Base4Dim<2, 2, 1>	
{	
	enum { is_conv_factor = 1 };
	static constexpr double factor = 0.001;
};	
template <>	
struct ulib::ung::factors::Base2Base4Dim<2, 1, 2>
{
	enum { is_conv_factor = 1 };
	static constexpr double factor = 1000;
};	

在这种情况下,第一个模板参数 2 代表 MASS 的基本维度。

当你写作

ulib_Secondary_base_adopt_base_unit(cgs, secs)

生成以下内容

template <>		
struct ulib::ung::factors::Base2Base4Dim<3, 2, 1>	
{	
	enum { is_conv_factor = 0 };
	static constexpr double factor = 1;
};	
template <>	
struct ulib::ung::factors::Base2Base4Dim<3, 1, 2>
{
	enum { is_conv_factor = 0 };
	static constexpr double factor = 1.0;
};

在这种情况下,第一个模板参数 3 代表 TIME 的基本维度,并且 is_conv_factor = 0factor = 1.0 表明 MKS 和 cgs 在 TIME 维度上存在同一性。它们都使用 secs

我们现在拥有实现 Sys2Sys<> 结构所需的所有组件,用于 MKS (1) 和 cgs (2) 之间任何机械测量维度的转换。例如,对于 FORCE,它是 dimensions<1, 1, -2, ...>,将实例化以下 Sys2Sys<> 结构

template <> struct Sys2Sys<FORCE, 1, 2>;

factor 成员由我们定义的 Base2Base4Dim<> 特化构建。

static constexpr double factor = 
	int_power<1>::of(Base2Base4Dim<1, 1, 2>::factor)
	* int_power<1>::of(Base2Base4Dim<2, 1, 2>::factor)
	* int_power<-2>::of(	Base2Base4Dim<3, 1, 2>::factor);

以同样的方式,用于反方向转换的 Sys2Sys<> 结构

template <> struct Sys2Sys<FORCE, 2, 1>;

所有这些 unit_to_unit<> 的计算都在编译期间进行。结果将是在运行时代码中插入一个单一的因子乘法来执行转换,或者不插入任何东西。这是通过使用与上面描述的 convert_to_base<>() 函数相同的模式来完成的。

        template<class To>
        struct convert_to 
        {
        private:
            template<bool, class From> struct no_conv_factor
            {
                static constexpr double convert(From const& from)
                {
                    return from.Value();
                }
            };
            template<class From> struct no_conv_factor<false, From>
            {
                static constexpr double convert(From const& from)
                {
                    return factors::unit_to_unit<From, To>::factor 
                                                        * from.Value();
                }
            };
        public:
            template<class From>
            static constexpr double from(From const& from)
            {
                return no_conv_factor
                    <
                    false == factors::unit_to_unit<From, To>::is_conv_factor,
                    From
                    >::convert(from);
            }
        };

//called as follows
convert_to<metres>::from(2.5_Kms);

 

一般评论

关于它是如何工作的,还有更多的细节,但我认为有了上述的指导,你可以通过检查代码来弄清楚。

我没有一次性写对这个代码。这是一个对原始库代码进行重新设计和重构的过程,通过增量更改,始终保持其可用性和可测试性。我也没有一次性设计正确。很多时候,我不得不回溯之前的设计决策,即使在开发的后期阶段也是如此。

我已尽力使其运行时效率最佳,并且相信我已成功。这是通过在编译期间预先计算所有已知编译时信息来实现的。这与量纲分析一起意味着编译器将比使用原始数字而不是单位类型量时更努力地工作。

C++ 语言才刚刚开始意识到它自身通过引入模板而偶然出现的编译时编程能力。
constexpr 符号、constexpr 函数和模板 using 是语言标准对这一点的初步认可。

C++ 11 使得能够进行所有必要的编译时编程,但并非总是以你想象的最有效的方式。此外,在可读性和编译时效率之间权衡时,我选择了可读性。尽管如此,编译时负担是线性的,没有可能阻塞编译器的指数过程或深度递归。让编译器为你做一些工作是很有意义的——检查正确性和预计算所有所需的转换。这正是它的用途。

C++14 开始为编译时编程打开大门,使其更具可读性且编译效率更高——例如,通过扩展 constexpr 限定函数中可以完成的操作。应用此功能(此库尚未应用)可能会在编译效率上产生一些改进,但我认为这会是微小的。C++ 11 是一个重要的转折点,没有迫切的理由为 C++14 重写它。

我仍然不得不跳过一些障碍,因为语言不允许我做我想做的事情。

  • 浮点数仍然不能作为模板参数。我已经描述了如何必须动态编写一个带有 constexpr static double 成员的类,该成员字面量初始化以解决这个问题。
  • 为了构建用于组合初始化的单位链表,我不得不求助于基于行号的模板函数特化。一个编译时变量,甚至只是一个计数器,都可以消除对这种复杂而晦涩的构造的需求。提供它应该不难。

我在编写这个库时非常小心,但即便在开发的最后阶段和测试期间,我仍然发现并排除了错误和拼写错误。我希望它完整且运行正常。我相信,任何仍然隐藏的棘手边缘情况一旦被发现,都可以迅速解决。我已经对架构进行了重大重构和根本性设计更改。它并不脆弱。

历史

这是该库针对 C++11 的第一个版本。它是基于 2014 年 9 月在 The Code Project 上发布的 C++ 中的测量单位类型 的开发版本,该版本仍可用于预 C++11 编译器。

© . All rights reserved.