Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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).

like image 791
alfC Avatar asked May 17 '20 19:05

alfC


2 Answers

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.

like image 136
IlCapitano Avatar answered Oct 04 '22 09:10

IlCapitano


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>().

like image 38
Zach Peltzer Avatar answered Oct 04 '22 09:10

Zach Peltzer