19

Does C++ offer something similar to Ada's subtype to narrow a type?

E.g.:

type Weekday is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
subtype Working_Day is Weekday range Monday .. Friday;
sornbro
  • 243
  • 3
  • 12
  • 3
    It is possible to write a wrapper class implementing proper range checking. Out of box there is nothing available of this kind. Note that even the possible values of enumerators in C++ are typically not limited to the listed "known values" so even defining `Weekday` is not possible. – user7860670 May 09 '19 at 21:21
  • @VTT The values are not really limited in Ada either: You could perform an unchecked_conversion (the equivalent to a C++ value static_cast) in Ada as well and screw up in the same way. It's just less idiomatic. Or would the conversion in Ada raise an exception? – Peter - Reinstate Monica May 09 '19 at 21:33
  • @PeterA.Schneider I'd say that `unchecked_conversion` is more like an library routine like memcpy in the way it forces compiler to assume things. – user7860670 May 09 '19 at 21:42
  • @VTT Yes, pretty much like a cast ;-). It's just that casts are common while unchecked_conversions are probably uncommon. – Peter - Reinstate Monica May 09 '19 at 21:58
  • C++'s built in `enum`s are ... limited. But there exist numerous attempt to make `enum` classes that do much more of what is desirable. Perhaps one of those offers a clear path forward. – dmckee --- ex-moderator kitten May 09 '19 at 22:43
  • 1
    @Peter A. Schneider An unchecked conversion from one enumerated type to another will cause an exception in Ada. The values of a discrete type (an enumerated type or an integer type) contain a discrete set of values. Forcing a value an enumerated type into a foreign enumerated type will raise an exception. – Jim Rogers May 09 '19 at 23:35
  • @JimRogers But converting certain out-of-range integers into an enumeration works just fine with GNAT, see https://www.jdoodle.com/execute-ada-online#&togetherjs=r4QutHrPPT. GNAT also accepts enum-to-enum conversions, even after an out-or-range assignment (same link). – Peter - Reinstate Monica May 10 '19 at 05:48
  • @PeterA.Schneider But the linked example assigns two natural numbers, it does not perform any conversions nor use enumerations. – user7860670 May 10 '19 at 06:23
  • @VTT Oh, the edits I made were apparently not saved to that link. Mabe I would have needed to create a new link. Sorry for that. I don't have access to the edited project from here, but I did an unchecked conversion from 10 to a weekday enumeration, and also from one enum to another. The conversion always "worked" (i.e. never threw), iirc; the attempt to `Put()` such a variable sometimes raised a constraint error (e.g. for 9 and -1), but not for 10. – Peter - Reinstate Monica May 10 '19 at 07:31
  • @JimRogers, UC between two enumerations _may_ cause an exception, at least when the result is used. I’d use a lookup table to avoid this. – Simon Wright May 10 '19 at 07:42
  • @VTT For what's it worth I saved the project so the link should show an unchecked_conversion now. – Peter - Reinstate Monica May 12 '19 at 21:28
  • There is [a follow up to this question](https://stackoverflow.com/q/56246573/3876684). – Claas Bontus Mar 24 '21 at 08:31

5 Answers5

7

No, not natively.

What you describe might be best represented as a scoped enum, accompanied by a separate scoped enum with a subset of enumerations who share numerical representations with the "parent" scoped enum.

You could further define some conversions between the two, but without reflection it's not really possible to make it all elegant and intuitive, at least not without hardcoding and duplicating loads of stuff which rather defeats the purpose.

It would be best, when programming C++, to attempt entirely abandoning the mindset imbued by programming in other languages.

That being said, this is actually quite a nice feature idea, though I wouldn't hold my breath!

Workaround: just use an enum, and apply range checking where you need to.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • I don't know Ada but I knew Pascal, which has a sub range type, probably because it was derived from Ada. It's handly but not quite extremely useful – phuclv May 12 '19 at 15:10
  • 2
    @phuclv In Ada ranged types are far more complete than most Pascal-ish languages. An enumeration is a valid array index. One consequence is that EVERY out-of-bounds array access is a simple type error. Many can be caught at compile time with no cost; the rest typically raise an exception pointing spookily close to the bug. Quite extremely useful. –  May 12 '19 at 17:32
  • 2
    @phuclv Pascal came first – Simon Wright May 12 '19 at 20:53
  • Ada came around 13 years after Pascal ! – Zerte May 15 '19 at 07:29
5

There are a few additional differences between C++ enumerations and Ada enumerations. The following Ada code demonstrates some of these differences.

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;

The output of this program is:

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

The subtype Work_Days has an is-a relationship with the type Days. Every member of Work_Days is also a member of Days. In this example the set of valid values for Work_Days is a subset of the set of valid values for Days.

Characters in Ada are defined as an enumeration. It is therefore simple to define subtypes of the type Character for special uses. The following example reads text from a file and counts the number of occurrences of upper case letters and lower case letters, ignoring all other characters in the file.

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;

Two subtypes of Character are defined. The subtype Upper_Case contains the range of Character values from 'A' through 'Z', while the subtype Lower_Case contains the range of Character values from 'a' through 'z'.

Two arrays are created for counting the letters read. The array Uppers is indexed by the set of Upper_Case values. Each element of the array is an instance of Natural, which is a pre-defined subtype of Integer containing only non-negative values. The array Lowers is indexed by the set of Lower_Case values. Each element of Lowers is also an instance of Natural.

The program prompts for a file name, opens that file, then reads the file one line at a time. The characters in each line are parsed. If the character is an Upper_Case character the array element in Uppers indexed by the parsed letter is incremented. If the character is a Lower_Case character the array element in Lowers indexed by the parsed letter is incremented.

The following output is the result of reading the source file for the count_letters program.

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
Jim Rogers
  • 4,822
  • 1
  • 11
  • 24
4

What you want might (at least partially) be realized using std::variant introduced with C++17.

struct Monday {};
struct Tuesday {};
/* ... etc. */
using WeekDay= std::variant<Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday>;

The following code defines sub_variant_t which constructs a new variant from the submitted type. E.g. using Working_Day= sub_variant_t<WeekDay,5>; takes the first five elements from 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;

If you want to copy values from the smaller type (Working_Day) to the larger one (Weekday) you can use WeekDay d3= var2var<WeekDay>( d1 ); where var2var is defined as follows.

template<class toT, class... Types>
toT
var2var( std::variant<Types...> const & v )
{
    return std::visit([](auto&& arg) -> toT {return toT(arg);}, v);
}

See this livedemo.

Claas Bontus
  • 1,628
  • 1
  • 15
  • 29
-1

Probably you can overload the assignation with a postcondition

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

Purely theoretical. Have not tried myself. But what do you guys think?

But is funny to appreciate how every upgrade on C++ they push as advanced features all the stuff Ada programmers take for granted.

julio
  • 11
  • 1
-3

Range checking has a cost. C++ has a zero cost policy for features: if you want the feature and you should pay a cost for it, you need to be explicit. That being said, mostly you can use some library or write your own.

Also, what do you expect when someone tries to put Sunday to Working_Day? An exception (most likely)? To set it to Monday? To set it to Friday? Invalidate the object? Keep the same value and ignore that (bad idea)?

As an example:

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

which outputs:

1
2
Exception Out of range
2
Mirko
  • 1,043
  • 6
  • 12
  • 3
    I think the paragraph about `zero cost policy for features` is misleading. It's ok to just say `c++ doesn't have an exact analog to xyz feature` – PeterT May 10 '19 at 08:45
  • 1
    C++ could have introduced `short enum class : Weekday { Weekday::Monday, Weekday::Tuesday };` at no runtime cost ;) – Lightness Races in Orbit May 10 '19 at 10:27
  • In most cases range checking won't be necessary (typically, variables of "for" loops, or copying a value of a certain subtype to a variable of the same or a larger subtype) and good Ada compilers won't add those unnecessary checks. – Zerte May 11 '19 at 14:14
  • Additionally, the benefits of subtypes appear most of the time at compile-time - no run-time cost there! – Zerte May 11 '19 at 14:16