c++ 编写一个只接受文字'0'或文字'1'作为参数的函数

xu3bshqb  于 2022-12-20  发布在  其他
关注(0)|答案(8)|浏览(160)

有时对于代数类型,即使底层类型不是整数,也可以使用一个构造函数来表示中性元素,或者表示乘法单位元素。
问题是,如何说服编译器只接受01而不接受任何 * 其他 * 整数并不明显。
在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++ pre11中,人们有能力做到这个技巧,只接受一个零。

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 literal _c来创建std::integral_constant<int, 0>std::integral_constant<int, 1>的示例,然后让函数采用这些类型,但是对于0的情况,最终的语法会是最糟糕的,也许有更简单的方法。

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

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

unftdfkk

unftdfkk1#

在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那样给出错误,但是godbolt上的clang (trunk)给出了错误。

wyyhbhjk

wyyhbhjk2#

可以通过将0或1作为模板参数传递来实现,如下所示:

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

函数的调用方式如下:我不相信同样的事情可以对构造函数做(因为你不能显式地为构造函数设置模板参数),但是你可以使构造函数私有化,并让静态 Package 函数可以被赋予模板参数来执行检查:

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>()创建。

kadbb459

kadbb4593#

那么...你已经标记了C++17,所以你可以使用if constexpr
因此,当0_xstd::integral_constant<int, 0>值、1_xstd::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);
p5cysglq

p5cysglq4#

对于Ada,可以定义子类型、新类型或仅对Integer 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

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

wribegjk

wribegjk5#

这不是一个现代的解决方案,但是添加到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
}

尽管如此,对于构造函数问题,您可以将类模板化,而不是尝试使用模板化的构造函数

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

pxiryf3j6#

迄今为止,我发现接受字面量0的最佳解决方案是使用std::nullptr_t作为函数的输入:

struct math_object
{
    real x,y,z;
    math_object(std::nullptr_t) : x(0), y(0), z(0) {}
};

与其他一些解决方案相比,这具有转换优势。例如,它允许使用.. void MyFunc(const math_object &obj=0);这样的语法。我已经使用它很多年了,没有发现任何问题。但是,我没有类似的解决方案来处理文字1。为此,我创建了一个具有全局IDENTITY变量的construct::id结构。

inkz8wg9

inkz8wg97#

这是一个基本的问题,在编译器中,如何对一个参数进行编译,同时又是有效的,那么,你到底需要什么呢?
这包含在Pascal或Ada等强类型语言中。枚举类型只有两个值,通常在开发时检查类型,但在运行时,某些编译器选项会取消检查,因为 * 一切顺利 *。
函数接口是一个契约。它是卖方与卖方之间的契约(函数的编写者)和买方(该函数的用户)。甚至有一个仲裁器,这是一种编程语言,如果有人试图欺骗合同,它可以采取行动。但最终,程序运行在一台机器上,这台机器可以随意修改枚举值集,并完全放置在(和不允许的值)。
这个问题也伴随着单独编译而来。单独编译有它的缺点,因为它必须面对一个编译,而不需要重新检查和重新测试你之前做过的所有编译。一旦编译完成,你放入代码中的所有东西都在那里。如果你想让代码高效,那么测试就是多余的,因为调用者和实现者都要科普契约,但是如果你想捕捉一个谎言,那么你必须包含测试代码。然后,最好对所有情况都做一次,或者最好让程序员决定我们何时想捕捉一个谎言?
C语言的问题他们的灵感来自于优秀的程序员,他们不会出错,他们的软件必须在又大又慢的机器上运行。他们决定同时开发两种语言(第二个是为了互操作性)弱类型......它们确实如此。你试过用Ada?或Modula-2编程吗?随着时间的推移,你会发现,强打字的东西比其他东西更学术,最后你想要的,作为一个专业人士,是有自由说:现在我希望安全(并包含测试代码),现在我知道我在做什么(请尽可能高效)

结论

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

bfrts1fy

bfrts1fy8#

这是我的问题的答案,基于@IlCapitano对一个 Package 类的回答。这个 Package 类可以私有化,并且只在构造时使用。

class Matrix {
    struct ZeroOROne {
        /*implicit*/ consteval ZeroOROne(int n) : val_{n} {
            if (n != 0 and n != 1) {throw;}  // throw can produce an error at compile time
        }
        int val_;
    };

 public:
    constexpr Matrix(ZeroOROne _0_or_1) {
        if(_0_or_1.val_ == 0) {  // this cannot be if constexpr, but that is ok
            a00 = a01 = a10 = a11 = 0.0;
            new int{0};  // allocation could is ok here
        } else {
            a00 = a11 = 1.0;
            a10 = a01 = 0.0;
            new int{0};  // allocation could is ok here
        }
    }
    double a00; double a01;
    double a10; double a11;
};

这样,只允许Matrix A(0)Matrix A(1)。(虽然它也适用于常量变量,但这是可以的。)

int main() {
    // ZeroOROne(0);
    // ZeroOROne(1);
    // ZeroOROne(2);  // compilation error

    Matrix A(0);
    Matrix B(1);

    // Matrix C(2);  // compilation error

    int const d = 0;  // this works because the compiler can "see" the 0.
    Matrix D(d);

    constexpr int e = 0;
    Matrix E(e);

    // int f = 0;
    // Matrix F(f);  // compile error

    return B.a00;
}

这里显示了构造函数中的“运行时”if不是问题,可以由编译器省略:https://godbolt.org/z/hd6TWY6qW
该解决方案需要C++20和GCC和clang的最新版本中工作。

相关问题