Write a function that only accepts literal `0` or literal `1` as argument

Viewed 649

Sometimes for algebraic types it is convenient to have a constructor that takes a literal value 0 to denote the neutral element, or 1 to denote the multiplicative identity element, even if the underlying type is not an integer.

The problem is that it is not obvious how to convince the compiler only to accept, 0 or 1 without accepting any other integer.

Is there a way to do this in C++14 or beyond, for example combining literals, constexpr or static_assert?

Let me illustrate with a free function (although the idea is to use the technique for a constructor that take a single argument. Contructors cannot take template parameters either).

A function that accepts zero only could be written in this way:

constexpr void f_zero(int zero){assert(zero==0); ...}

The problem is that, this could only fail at runtime. I could write f_zero(2) or even f_zero(2.2) and the program will still compile.

The second case is easy to remove, by using enable_if for example

template<class Int, typename = std::enable_if_t<std::is_same<Int, int>{}> >
constexpr void g_zero(Int zero){assert(zero==0);}

This still has the problem that I can pass any integer (and it only fails in debug mode).

In C++ pre 11 one had the ability to do this trick to only accept a literal zero.

struct zero_tag_{}; 
using zero_t = zero_tag_***;
constexpr void h_zero(zero_t zero){assert(zero==nullptr);}

This actually allowed one to be 99% there, except for very ugly error messages. Because, basically (modulo Maquevelian use), the only argument accepted would be h_zero(0).

This is situation of affairs is illustrated here https://godbolt.org/z/wSD9ri . I saw this technique being used in the Boost.Units library.

1) Can one do better now using new features of C++?

The reason I ask is because with the literal 1 the above technique fails completely.

2) Is there an equivalent trick that can be applied to the literal 1 case? (ideally as a separate function).

I could imagine that one can invent a non-standard long long literal _c that creates an instance of std::integral_constant<int, 0> or std::integral_constant<int, 1> and then make the function take these types. However the resulting syntax will be worst for the 0 case. Perhaps there is something simpler.

f(0_c);
f(1_c);

EDIT: I should have mentioned that since f(0) and f(1) are potentially completely separate functions then ideally they should call different functions (or overloads).

6 Answers

You can get this by passing the 0 or 1 as a template argument like so:

template <int value, typename = std::enable_if_t<value == 0 | value == 1>>
void f() {
    // Do something with value
}

The function would then be called like: f<0>(). I don't believe the same thing can be done for constructors (because you can't explicitly set template parameters for constructors), but you could make the constructor(s) private and have static wrapper functions which can be given template parameters perform the check:

class A {
private:
    A(int value) { ... }

public:
    template <int value, typename = std::enable_if_t<value == 0 || value == 1>>
    static A make_A() {
        return A(value);
    }
};

Objects of type A would be created with A::make_A<0>().

In C++20 you can use the consteval keyword to force compile time evaluation. With that you could create a struct, which has a consteval constructor and use that as an argument to a function. Like this:

struct S
{
private:
    int x;
public:
    S() = delete;

    consteval S(int _x)
        : x(_x)
    {
        if (x != 0 && x != 1)
        {
            // this will trigger a compile error,
            // because the allocation is never deleted
            // static_assert(_x == 0 || _x == 1); didn't work...
            new int{0};
        }
    }

    int get_x() const noexcept
    {
        return x;
    }
};

void func(S s)
{
    // use s.get_x() to decide control flow
}

int main()
{
    func(0);  // this works
    func(1);  // this also works
    func(2);  // this is a compile error
}

Here's a godbolt example as well.

Edit:
Apperently clang 10 does not give an error as seen here, but clang (trunk) on godbolt does.

Well... you have tagged C++17, so you can use if constexpr.

So you can define a literal type when 0_x is a std::integral_constant<int, 0> value, when 1_x is a std::integral_constant<int, 1> and when 2_x (and other values) gives a compilation error.

By example

template <char ... Chs>
auto operator "" _x()
 {
   using t0 = std::integer_sequence<char, '0'>;
   using t1 = std::integer_sequence<char, '1'>;
   using tx = std::integer_sequence<char, Chs...>;

   if constexpr ( std::is_same_v<t0, tx> )
      return std::integral_constant<int, 0>{};
   else if constexpr ( std::is_same_v<t1, tx> )
      return std::integral_constant<int, 1>{};
 }

int main ()
 {
   auto x0 = 0_x;
   auto x1 = 1_x;
   //auto x2 = 2_x; // compilation error

   static_assert( std::is_same_v<decltype(x0),
                                 std::integral_constant<int, 0>> );
   static_assert( std::is_same_v<decltype(x1),
                                 std::integral_constant<int, 1>> );
 }

Now your f() function can be

template <int X, std::enable_if_t<(X == 0) || (X == 1), bool> = true>
void f (std::integral_constant<int, X> const &)
 {
   // do something with X
 }

and you can call it as follows

f(0_x);
f(1_x);

This isn't a modern solution, but adding on to Zach Peltzer's solution, you can keep your syntax if you use macros...

template <int value, typename = std::enable_if_t<value == 0 | value == 1>>
constexpr int f_impl() {
    // Do something with value
    return 1;
}


#define f(x) f_impl<x>()

int main() {
    f(0); //ok
    f(1); //ok
    f(2); //compile time error
}

Though, with the constructor problem you could just make the class templated instead of trying to have a templated constructor

template<int value, typename = std::enable_if_t<value == 0 | value == 1>>
class A {
public:
    A() {
        //do stuff 
    }

};


int main() {
    A<0> a0;
    auto a1 = A<1>();
    // auto a2 = A<2>(); //fails!
}

For the case of Ada, you can define a subtype, a new type, or a derived type that is constrained only for the values of Integer 0 and 1.

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure two_value is

    -- You can use any one of the following 3 declarations. Just comment out other two.
    --subtype zero_or_one is Integer range 0 .. 1;  -- subtype of Integer.
    --type zero_or_one is range 0 .. 1;             -- new type.
    type zero_or_one is new Integer range 0 .. 1;   -- derived type from Integer.

    function get_val (val_1 : in zero_or_one) return Integer;

    function get_val (val_1 : in zero_or_one) return Integer is
    begin
        if (val_1 = 0) then
            return 0;
        else
            return 1;
        end if;

    end get_val;

begin

    Put_Line("Demonstrate the use of only two values");

    Put_Line(Integer'Image(get_val(0)));
    Put_Line(Integer'Image(get_val(1)));
    Put_Line(Integer'Image(get_val(2)));

end two_value;

upon compiling you get the following warning message, although compiles successfully :

>gnatmake two_value.adb
gcc -c two_value.adb
two_value.adb:29:40: warning: value not in range of type "zero_or_one" defined at line 8
two_value.adb:29:40: warning: "Constraint_Error" will be raised at run time
gnatbind -x two_value.ali
gnatlink two_value.ali

And executing it gives the runtime error as specified by the compiler

>two_value.exe
Demonstrate the use of only two values
 0
 1

raised CONSTRAINT_ERROR : two_value.adb:29 range check failed

So, basically you can constrain the values by defining the new types, derived types or subtypes, you don't need to include the code to check the range, but based on your data type the compiler will automatically warn you.

There's a basic problem. How can you do that in the compiler to be done for a parameter, and at the same time be efficient? Well, what do you need exactly?

That is included in strong typed languages like Pascal, or Ada. The enumerated types have only a couple of values, and the types are normally checked at development, but otherwise, the checks are eliminated by some compiler option at runtime, because just everything goes well.

A function interface is a contract. It is a contract between a seller (the writer of the function) and a buyer (the user of that function). There's even an arbiter, which is the programming language, that can act upon if someone tries to cheat the contract. But at the end, the program is being run in a machine that's open to make arbitraryness like modifying the set of enumerated values and put in the place a completely (and not permitted value).

The problem comes also with separate compilation. Separate compilation has its drawbacks, as it must face a compilation, without having to recheck and retest all previous compilations you have made. Once a compilation is finished, everything you have put in the code is there. If you want the code to be efficient, then the tests are superfluous, because caller and implementer both cope with the contract, but if you want to catch a lyer, then you have to include the test code. And then, it is better to do once for all cases, or is it better to let the programmer decide when and when not we want to catch a lyer?

The problem with C (and by legacy with C++) is that they were inspired by very good programmers, that didn't mistakes, and that have to run their software in big and slow machines. They decided to make both languages (the second was for interoperability purposes) weak typed... and so they are. Have you tried to program in Ada? or Modula-2? You'll see that, over the time, the strong typing thing is more academic than otherwise, and finally what you want, as a professional, is to have the freedom to say: now I want to be safe (and include test code), and now I know what I'm doing (and please be most efficient as you can)

Conclusion

The conclussion is that you are free to select the language, to select the compiler, and to relax the rules. The compilers have the possibility to allow you that. And you have to cope with it, or invent (this is something that todays happens almost each week) your own programming language.

Related