C++中的等效Ada子类型

19

C++是否提供类似于Ada的subtype来缩小类型范围的功能?

例如:

type Weekday is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
subtype Working_Day is Weekday range Monday .. Friday;

3
可以编写一个包装类来实现适当的范围检查,但目前没有现成的类可用。请注意,即使是C++中枚举器可能的值通常也不限于列出的“已知值”,因此即使定义“Weekday”也是不可能的。 - user7860670
C++内置的枚举类型有一定的限制。但是存在许多尝试创建更多所需功能的枚举类。也许其中一个提供了明确的前进道路。 - dmckee --- ex-moderator kitten
1
@Peter A. Schneider 在Ada中,从一个枚举类型到另一个枚举类型的未经检查转换将导致异常。离散类型(枚举类型或整数类型)的值包含一组离散的值。强制将枚举类型的值强制转换为外部枚举类型将引发异常。 - Jim Rogers
@JimRogers,两个枚举之间的UC可能会导致异常,至少在使用结果时是这样。我建议使用查找表来避免这种情况。 - Simon Wright
这个问题有一个后续问题 - Claas Bontus
显示剩余7条评论
5个回答

7
不能原生支持。您所描述的最好以作用域枚举的形式表示,伴随着一个子集作用域枚举,该子集与“父”作用域枚举共享数值表示。你可以进一步定义一些转换方法,但没有反射机制,就不可能使它变得优雅和直观,至少不需要硬编码和重复大量的工作,否则就失去了目的。在编写C++程序时,最好尝试完全放弃其他语言编程所注入的思维方式。话虽如此,这实际上是一个非常好的功能想法,尽管我不会抱有很大希望!解决方法:只需使用枚举,并在需要时应用范围检查。

我不知道Ada,但我知道Pascal,它有一个子范围类型,可能是因为它源于Ada。它很方便,但并不是非常有用。 - phuclv
2
@phuclv 在Ada中,范围类型比大多数类似Pascal的语言更完整。枚举是有效的数组索引。一个结果是每个越界的数组访问都是一个简单的类型错误。许多可以在编译时无成本地捕获;其余通常会引发异常,指向错误附近。非常有用。 - user1818839
2
@phuclv Pascal 首先出现 - Simon Wright
Ada出现在Pascal之后大约13年! - Zerte

5

C++枚举和Ada枚举之间存在一些其他的差异。以下Ada代码演示了其中一些差异。

with Ada.Text_IO; use Ada.Text_IO;

procedure Subtype_Example is
   type Days is (Monday, Tueday, Wednesday, Thursday, Friday, Saturday, Sunday);
   subtype Work_Days is Days range Monday..Friday;

begin
   Put_Line("Days of the week:");
   for D in Days'Range loop
      Put_Line(D'Image);
   end loop;
   New_Line;
   Put_Line("Days with classification:");
   for D in Days'Range loop
      Put(D'Image & " is a member of");
      if D in Work_Days then
         Put_Line(" Work_Days");
      else
         Put_Line(" a non-work day");
      end if;
   end loop;

end Subtype_Example;

这个程序的输出结果是:
Days of the week:
MONDAY
TUEDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY

Days with classification:
MONDAY is a member of Work_Days
TUEDAY is a member of Work_Days
WEDNESDAY is a member of Work_Days
THURSDAY is a member of Work_Days
FRIDAY is a member of Work_Days
SATURDAY is a member of a non-work day
SUNDAY is a member of a non-work day

Work_Days子类型与Days类型存在is-a关系。每个Work_Days的成员也是Days的成员。在这个例子中,Work_Days的有效值集合是Days的有效值集合的子集。

Ada语言中的字符被定义为枚举类型。因此,可以很容易地为特殊用途定义Character类型的子类型。以下示例从文件中读取文本,并计算大写字母和小写字母的出现次数,忽略文件中的所有其他字符。

with Ada.Text_IO; use Ada.Text_IO;

procedure Count_Letters is
   subtype Upper_Case is Character range 'A'..'Z';
   subtype Lower_Case is Character range 'a'..'z';

   Uppers : array(Upper_Case) of Natural;
   Lowers : array(Lower_Case) of Natural;

   File_Name : String(1..1024);
   File_Id   : File_Type;
   Length    : Natural;
   Line      : String(1..100);
begin
   -- set the count arrays to zero
   Uppers := (Others => 0);
   Lowers := (Others => 0);

   Put("Enter the name of the file to read: ");
   Get_Line(Item => File_Name,
            Last => Length);

   -- Open the named file
   Open(File => File_Id,
        Mode => In_File,
        Name => File_Name(1..Length));

   -- Read the file one line at a time
   while not End_Of_File(File_Id) loop
      Get_Line(File => File_Id,
               Item => Line,
               Last => Length);
      -- Count the letters in the line
      for I in 1..Length loop
         if Line(I) in Upper_Case then
            Uppers(Line(I)) := Uppers(Line(I)) + 1;
         elsif Line(I) in Lower_Case then
            Lowers(Line(I)) := Lowers(Line(I)) + 1;
         end if;
      end loop;
   end loop;
   Close(File_Id);

   -- Print the counts of upper case letters
   for Letter in Uppers'Range loop
      Put_Line(Letter'Image & " =>" & Natural'Image(Uppers(Letter)));
   end loop;

   -- print the counts of lower case letters
   for Letter in Lowers'Range loop
      Put_Line(Letter'Image & " =>" & Natural'Image(Lowers(Letter)));
   end loop;
end Count_Letters;

定义了两个字符子类型。 Upper_Case 子类型包含 字符值从 'A' 到 'Z' 的范围,而 Lower_Case 子类型包含 字符值从 'a' 到 'z' 的范围。创建了两个数组来计算读取的字母数。Uppers 数组由 Upper_Case 值集索引。数组每个元素都是 Natural 类型的实例,这是一个预定义的 Integer 子类型,只包含非负值。Lowers 数组由 Lower_Case 值集索引。Lowers 的每个元素也是Natural 类型的实例。程序提示文件名,打开该文件,然后逐行读取文件。解析每行中的字符。如果字符是一个 Upper_Case 字符,则增加 Uppers 中由解析字母索引的数组元素。如果字符是一个 Lower_Case 字符,则增加 Lowers 中由解析字母索引的数组元素。下面的输出是读取 count_letters 程序源文件的结果。
Enter the name of the file to read: count_letters.adb
'A' => 3
'B' => 0
'C' => 12
'D' => 0
'E' => 2
'F' => 13
'G' => 2
'H' => 0
'I' => 21
'J' => 0
'K' => 0
'L' => 36
'M' => 1
'N' => 9
'O' => 7
'P' => 4
'Q' => 0
'R' => 3
'S' => 2
'T' => 3
'U' => 9
'V' => 0
'W' => 0
'X' => 0
'Y' => 0
'Z' => 1
'a' => 51
'b' => 3
'c' => 8
'd' => 19
'e' => 146
'f' => 15
'g' => 16
'h' => 22
'i' => 50
'j' => 0
'k' => 0
'l' => 38
'm' => 13
'n' => 57
'o' => 48
'p' => 35
'q' => 0
'r' => 62
's' => 41
't' => 78
'u' => 19
'v' => 0
'w' => 12
'x' => 2
'y' => 6
'z' => 2

4
您想要的可能(至少部分地)可以使用C++17引入的std::variant来实现。
struct Monday {};
struct Tuesday {};
/* ... etc. */
using WeekDay= std::variant<Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday>;

下面的代码定义了sub_variant_t,它从提交的类型构造一个新的variant。例如,using Working_Day= sub_variant_t<WeekDay,5>;Weekday中取出前五个元素。
template<class T,size_t o,class S>
struct sub_variant_h;

template<class T,size_t o,size_t... I>
struct sub_variant_h<T,o,std::index_sequence<I...> >
{
    using type= std::variant<typename std::variant_alternative_t<(I+o),T>... >;
};

template<class T,size_t end, size_t beg=0>
struct sub_variant
{
    using type= typename sub_variant_h<T,beg,std::make_index_sequence<end-beg> >:type;
};

template<class T,size_t end, size_t beg=0>
using sub_variant_t = typename sub_variant<T,end,beg>::type;

如果您想将值从“较小”的类型(`Working_Day`)复制到“较大”的类型(`Weekday`),可以使用`WeekDay d3= var2var(d1);`,其中`var2var`定义如下。
template<class toT, class... Types>
toT
var2var( std::variant<Types...> const & v )
{
    return std::visit([](auto&& arg) -> toT {return toT(arg);}, v);
}

查看此在线演示

3
任何足够先进的元编程都难以区分于魔法。 - Peter - Reinstate Monica
看起来在这个设置中,“Working_Day”和“Weekday”将是不相关的类型。 - user7860670

-1

你可能可以使用后置条件来重载赋值操作符

Ensures(result > 0 && result < 10);  

纯理论。 我自己还没有尝试过。 但是你们觉得呢?
但有趣的是,要欣赏每次 C++ 升级,它们都将 Ada 程序员视为理所当然的高级特性

-3

范围检查是有成本的。C++对功能有零成本策略:如果你想要这个功能并且应该为此付出代价,那么你需要明确表示。话虽如此,大多数情况下你可以使用一些库或编写自己的库。

另外,当有人试图将Sunday放入Working_Day时,你期望发生什么?抛出异常(最可能的)?将其设置为Monday?将其设置为Friday?使对象无效?保持相同的值并忽略它(不好的想法)?

例如:

#include <iostream>
#include <string>
using namespace std;

enum class Weekday
{
    Sunday= 0,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
};

template <class T, T min, T max>
class RangedAccess
{
    static_assert(max >= min, "Error min > max");
private:
    T t;

    public:
    RangedAccess(const T& value= min)
    {
        *this= value;
    }


    RangedAccess& operator=(const T& newValue)
    {
        if (newValue > max || newValue < min) {
            throw string("Out of range");
        }
        t= newValue;
    }

    operator const T& () const
    { 
        return t; 
    }

    const T& get() const
    { 
        return t; 
    }
};

using Working_Day= RangedAccess<Weekday, Weekday::Monday, Weekday::Friday>;

int main()
{
    Working_Day workday;

    cout << static_cast<int>(workday.get()) << endl; // Prints 1
    try {
        workday= Weekday::Tuesday;
        cout << static_cast<int>(workday.get()) << endl; // Prints 2
        workday= Weekday::Sunday; // Tries to assign Sunday (0), throws
        cout << static_cast<int>(workday.get()) << endl; // Never gets executed

    } catch (string s) {
        cout << "Exception " << s << endl; // Prints "Exception out of range"
    }
    cout << static_cast<int>(workday.get()) << endl; // Prints 2, as the object remained on Tuesday
}

这将输出:

1
2
Exception Out of range
2

3
我认为关于“零成本策略”的段落有误导性。只说“C++没有与XYZ功能完全相似的模拟”就可以了。 - PeterT
1
C++ 可以在没有运行时成本的情况下引入 short enum class : Weekday { Weekday::Monday, Weekday::Tuesday }; ;) - Lightness Races in Orbit
在大多数情况下,不需要进行范围检查(通常是“for”循环的变量,或将某个子类型的值复制到相同或更大子类型的变量中),而且良好的Ada编译器也不会添加这些不必要的检查。 - Zerte
此外,子类型的好处大多数情况下出现在编译时 - 没有运行时成本! - Zerte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接