编写一个只接受文字“0”或文字“1”作为参数的函数

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

有时对于代数类型,使用一个构造函数会很方便,该构造函数采用文字值 0 来表示中性元素,或 1 来表示乘法单位元素,即使基础类型不是整数。

问题是如何说服编译器只接受 01 而不接受任何 other 整数并不明显。

有没有办法在 C++14 或更高版本中执行此操作,例如组合文字、constexpr 或 static_assert?

让我用一个自由函数来说明(尽管我的想法是将这种技术用于采用单个参数的构造函数。构造函数也不能采用模板参数)。

一个只接受零的函数可以这样写:

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

问题是,这只能在运行时失败。我可以写 f_zero(2) 甚至 f_zero(2.2) 程序仍然可以编译。

第二种情况很容易去除,例如使用enable_if

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

这仍然存在我可以传递任何整数的问题(而且它只在调试模式下失败)。

在 C++ pre 11 中,可以使用此技巧来只接受文字零。

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

这实际上允许 99% 的人在那里,除了非常难看的错误消息。 因为,基本上(模 Maquevelian 使用),唯一接受的参数是 h_zero(0).

这里是事态说明https://godbolt.org/z/wSD9ri。 我看到 Boost.Units 库中使用了这种技术。

1) 现在可以使用 C++ 的新功能做得更好吗?

我问的原因是因为使用文字 1 上述技术完全失败。

2) 是否有可以应用于文字 1 案例的等效技巧?(最好作为一个单独的函数)。

我可以想象可以发明一个非标准的 long long 文字 _c 来创建 std::integral_constant<int, 0>std::integral_constant<int, 1> 的实例,然后使函数采用这些类型。但是,对于 0 情况,生成的语法将是最糟糕的。也许有更简单的东西。

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

编辑:我应该提到,由于 f(0)f(1) 可能是完全独立的函数,因此理想情况下它们应该调用不同的函数(或重载) .

您可以通过将 0 或 1 作为模板参数传递来获得此信息,如下所示:

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

函数将被调用为:f<0>()。我不相信可以为构造函数做同样的事情(因为你不能为构造函数显式设置模板参数),但你可以将构造函数设为私有并具有静态包装函数,可以给模板参数执行检查:

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);
    }
};

A 类型的对象将使用 A::make_A<0>() 创建。

这不是一个现代解决方案,但添加到 Zach Peltzer 的解决方案中,如果您使用宏,您可以保留您的语法...

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
}

不过,对于构造函数问题,您可以将 class 模板化,而不是尝试使用模板化构造函数

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

嗯...你已经标记了 C++17,所以你可以使用 if constexpr

所以当0_x是一个std::integral_constant<int, 0>值,当1_x是一个std::integral_constant<int, 1>,当2_x(以及其他值)给出编译错误。

举例

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>> );
 }

现在你的f()函数可以

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
 }

你可以这样调用它

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

在 C++20 中,您可以使用 consteval 关键字强制编译时求值。有了它,您可以创建一个结构,它有一个 consteval 构造函数并将其用作函数的参数。像这样:

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
}

这里还有一个godbolt example

编辑:
显然 clang 10 不会像 here 那样给出错误,但是 clang (trunk) 在 godbolt 上会出现错误。

有一个基本问题。您如何才能在编译器中为一个参数完成这些工作,同时又高效呢?那么,您到底需要什么?

这包含在 Pascal 或 Ada 等强类型语言中。枚举类型只有几个值,通常在开发时检查类型,但除此之外,检查会在 运行 时被某些编译器选项消除,因为 一切顺利.

一个函数接口就是一个契约。它是卖方(功能的编写者)和买方(该功能的用户)之间的合同。甚至还有一种仲裁程序,即编程语言,如果有人试图欺骗合约,它可以采取行动。但最后,该程序 运行 在一台开放的机器中进行任意操作,例如修改枚举值集并放置一个完整的(且不允许的值)。

单独编译也有问题。单独编译有它的缺点,因为它必须面对一个编译,而不必重新检查和重新测试你以前所做的所有编译。编译完成后,您放入代码中的所有内容都在那里。如果你希望代码高效,那么测试是多余的,因为调用者和实现者都处理契约,但如果你想抓住一个谎言,那么你必须包含测试代码。然后,对所有情况都做一次更好,还是让程序员决定什么时候和什么时候不想抓 lyer 更好?

C 的问题(以及 C++ 的遗留问题)是他们受到非常优秀的程序员的启发,他们没有错误,并且必须 运行 他们的软件在大而慢的机器上。他们决定使这两种语言(第二种语言是为了互操作性目的)成为弱类型的……它们就是这样。你试过用 Ada 编程吗?还是 Modula-2?你会发现,随着时间的推移,强类型的东西比其他东西更学术,最后,作为专业人士,你想要的是自由地说:现在我想要安全(并包括测试代码) ,现在我知道我在做什么了(请尽可能高效)

结论

结论是你可以自由select语言,select编译器,放宽规则。编译器有可能允许你这样做。你必须应对它,或者发明(这是今天几乎每周都会发生的事情)你自己的编程语言。

对于 Ada,您可以定义子类型、新类型或仅受整数 0 和 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;

编译时您会收到以下警告消息,尽管编译成功:

>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

执行它会出现编译器指定的运行时错误

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

raised CONSTRAINT_ERROR : two_value.adb:29 range check failed

所以,基本上你可以通过定义新类型、派生类型或子类型来约束值,你不需要包含检查范围的代码,但编译器会根据你的数据类型自动警告你.